cit dependency tracking and concurrency control.
Step 1: Define the Planner Interface
The planner receives the high-level objective and returns a structured dependency graph. It does not invoke tools. Its sole responsibility is step decomposition, input mapping, and dependency resolution.
interface WorkflowStep {
id: string;
tool: string;
inputs: Record<string, unknown>;
dependsOn: string[];
requiresExploration: boolean;
}
interface ExecutionPlan {
goal: string;
steps: WorkflowStep[];
metadata: { estimatedTokens: number; parallelBatches: number };
}
type PlannerFn = (objective: string) => Promise<ExecutionPlan>;
Step 2: Implement the Concurrent Executor
The executor traverses the dependency graph using topological sorting. Steps with no unresolved dependencies are batched and executed concurrently. Each step is a direct tool invocation, bypassing the reasoning loop entirely.
class StepExecutor {
private toolRegistry: Map<string, ToolFn>;
private results: Map<string, unknown> = new Map();
constructor(tools: Record<string, ToolFn>) {
this.toolRegistry = new Map(Object.entries(tools));
}
async execute(plan: ExecutionPlan): Promise<Record<string, unknown>> {
const sorted = this.topologicalSort(plan.steps);
for (const batch of this.groupByParallelism(sorted)) {
const promises = batch.map(step => this.runStep(step));
await Promise.all(promises);
}
return Object.fromEntries(this.results);
}
private async runStep(step: WorkflowStep): Promise<void> {
const resolvedInputs = this.resolveDependencies(step.inputs);
const tool = this.toolRegistry.get(step.tool);
if (!tool) throw new Error(`Tool ${step.tool} not registered`);
const output = step.requiresExploration
? await this.runLeafAgent(step, resolvedInputs)
: await tool(resolvedInputs);
this.results.set(step.id, output);
}
private async runLeafAgent(step: WorkflowStep, inputs: Record<string, unknown>): Promise<unknown> {
// Scoped reactive loop for exploratory sub-tasks
const leafAgent = new LeafReActAgent({ maxIterations: 3 });
return leafAgent.run({ goal: step.id, inputs, tool: step.tool });
}
private topologicalSort(steps: WorkflowStep[]): WorkflowStep[] {
const visited = new Set<string>();
const sorted: WorkflowStep[] = [];
const visit = (step: WorkflowStep) => {
if (visited.has(step.id)) return;
step.dependsOn.forEach(depId => {
const dep = steps.find(s => s.id === depId);
if (dep) visit(dep);
});
visited.add(step.id);
sorted.push(step);
};
steps.forEach(visit);
return sorted;
}
private groupByParallelism(sorted: WorkflowStep[]): WorkflowStep[][] {
const batches: WorkflowStep[][] = [];
const completed = new Set<string>();
const remaining = [...sorted];
while (remaining.length > 0) {
const ready = remaining.filter(s =>
s.dependsOn.every(d => completed.has(d))
);
if (ready.length === 0) throw new Error('Circular dependency detected');
batches.push(ready);
ready.forEach(s => completed.add(s.id));
remaining.splice(0, remaining.length, ...remaining.filter(s => !ready.includes(s)));
}
return batches;
}
}
Step 3: Synthesize Outputs
The synthesizer aggregates step results according to a template or transformation rule. It receives only the final outputs, not the execution history, preserving context efficiency.
class OutputSynthesizer {
assemble(plan: ExecutionPlan, results: Record<string, unknown>): string {
const orderedOutputs = plan.steps.map(step => results[step.id]);
return this.formatReport(orderedOutputs);
}
private formatReport(outputs: unknown[]): string {
// Domain-specific formatting logic
return outputs.map((o, i) => `## Step ${i + 1}\n${JSON.stringify(o)}`).join('\n\n');
}
}
Architecture Decisions & Rationale
- Decoupled Planner/Executor: Separating reasoning from execution eliminates context drift. The planner establishes the roadmap once; the executor follows it deterministically. This reduces token consumption by 40-60% compared to reactive loops that re-reason every iteration.
- Topological Execution: Dependency-aware scheduling prevents race conditions and enables safe parallelization. Steps that don't share data sources run concurrently, collapsing serial latency.
- Hybrid Leaf Nodes: Not all steps are deterministic. By marking specific steps as
requiresExploration, the system spawns scoped reactive sub-agents only where necessary. This preserves the predictability of the main pipeline while retaining flexibility for open-ended analysis.
- Result Isolation: The synthesizer receives only step outputs, not intermediate reasoning or tool logs. This minimizes context window pressure and simplifies error tracing.
Pitfall Guide
1. Prompt Engineering Over Architecture
Explanation: Teams attempt to fix loop-death by adding constraints, examples, or longer system prompts. Reactive loops lack structural state management, so prompt tweaks only delay the inevitable context drift.
Fix: Audit the task structure first. If the workflow has 4+ steps with known dependencies, switch to plan-and-execute before adjusting prompts.
2. Monolithic Dependency Graphs
Explanation: Planners sometimes generate flat step lists without explicit dependency mapping. This forces serial execution and eliminates parallelization opportunities.
Fix: Require the planner to output explicit dependsOn arrays. Validate the graph for cycles before execution. Use topological sorting to enforce correct ordering.
3. Unhandled Execution Failures
Explanation: Assuming all tool calls succeed leads to silent pipeline breaks. A single failed step can cascade into missing data for downstream steps.
Fix: Implement step-level retry logic with exponential backoff. Add fallback values or circuit breakers for non-critical steps. Log failure reasons separately from success paths.
4. Hybrid Overcomplication
Explanation: Marking every step as exploratory defeats the purpose of planning. Spawning reactive sub-agents for trivial lookups increases latency and token costs without improving accuracy.
Fix: Reserve requiresExploration flags for steps involving ambiguous data, subjective analysis, or multi-source correlation. Keep deterministic steps as direct tool invocations.
5. Context Window Bleed in Synthesis
Explanation: Passing full execution logs, tool responses, and intermediate reasoning to the synthesizer inflates context usage and introduces noise.
Fix: Strip execution metadata before synthesis. Pass only the final step outputs and the original goal. Use structured templates to guide formatting without consuming extra tokens.
6. Static Plans for Dynamic Data
Explanation: Hardcoded plans fail when upstream data changes structure or availability. The executor blindly follows a roadmap that no longer matches reality.
Fix: Implement plan validation gates. Before execution, run a lightweight schema check or data availability probe. Trigger a re-plan if critical inputs are missing or malformed.
7. Ignoring Cost Forecasting
Explanation: Teams deploy planning architectures without tracking token budgets per step. Unexpected exploratory leaves or retry loops cause cost overruns.
Fix: Attach estimated token counts to each step during planning. Monitor actual consumption against estimates in production. Set hard caps on leaf-agent iterations and fallback to deterministic paths when limits are approached.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Exploratory research or debugging | Reactive Loop | Path is unknown; each observation dictates the next move | Low upfront, high variance per run |
| Multi-step reporting or ETL | Plan-and-Execute | Structure is fixed; parallelization reduces latency | Predictable, 40-60% lower than reactive |
| Real-time monitoring or trading | Reactive Loop | Environment changes rapidly; plans become stale instantly | Moderate, scales with event frequency |
| Cost-sensitive pipeline | Plan-and-Execute | Fixed step count enables precise token budgeting | Highly predictable, easy to cap |
| Mixed workflow (data pull + analysis) | Hybrid Pattern | Deterministic steps run fast; exploratory leaves handle ambiguity | Moderate, optimized by scoping leaves |
Configuration Template
// agent.config.ts
import { WorkflowPlanner } from './planner';
import { StepExecutor } from './executor';
import { OutputSynthesizer } from './synthesizer';
import { registerTools } from './tools';
export const agentPipeline = {
planner: new WorkflowPlanner({
model: 'claude-sonnet-4-20250514',
maxSteps: 8,
requireDependencies: true,
tokenBudget: 4000
}),
executor: new StepExecutor({
tools: registerTools(['db_query', 'api_fetch', 'calc_churn', 'format_csv']),
maxRetries: 2,
parallelBatchSize: 4,
leafAgentConfig: {
model: 'gpt-4o-mini',
maxIterations: 3,
temperature: 0.3
}
}),
synthesizer: new OutputSynthesizer({
template: 'weekly_report_v2',
stripMetadata: true,
outputFormat: 'markdown'
}),
observability: {
trackTokenUsage: true,
logStepLatency: true,
alertOnIterationCap: true,
metricsEndpoint: '/api/v1/agent/metrics'
}
};
Quick Start Guide
- Define your workflow boundaries: List the inputs, outputs, and intermediate steps. Mark which steps require exploration versus direct tool execution.
- Initialize the planner: Pass your objective to the planner interface. Verify it returns a dependency graph with explicit
dependsOn arrays and step classifications.
- Register your tools: Map tool names to implementation functions. Ensure each tool accepts structured inputs and returns serializable outputs.
- Execute and synthesize: Run the executor against the plan. Pass the aggregated results to the synthesizer for final formatting. Monitor convergence metrics and adjust leaf-agent caps if needed.