Implementation Status
The FeatureAgent LangGraph graph is implemented at
packages/core/src/infrastructure/services/agents/feature-agent/. This guide covers LangGraph concepts and patterns used in the agent system. Some advanced examples (multi-agent supervisor, tool bindings) describe planned extensions.See AGENTS.md for the full current implementation details.
Guide to understanding and extending Shep’s LangGraph-based agent system.
Shep will use LangGraph for multi-agent orchestration. LangGraph provides:
import { createFeatureGraph } from '@/infrastructure/services/agents/feature-agent/graphs/feature.graph';
const workflow = createFeatureGraph();
const result = await workflow.invoke({
repoPath: '/path/to/repo',
featureDescription: 'Add user authentication with OAuth',
});
console.log(result.tasks); // Generated tasks
console.log(result.artifacts); // Generated PRD, RFC, etc.
const stream = await workflow.stream({
repoPath: '/path/to/repo',
featureDescription: 'Add dark mode toggle',
});
for await (const event of stream) {
console.log('Node:', event.node);
console.log('State update:', event.state);
}
State is a typed object passed through the graph:
import { Annotation } from '@langchain/langgraph';
export const FeatureState = Annotation.Root({
// Primitive fields
repoPath: Annotation<string>,
featureId: Annotation<string>,
// Object fields
repoAnalysis: Annotation<RepoAnalysis | null>,
plan: Annotation<Plan | null>,
// Array fields with reducers (append-only)
requirements: Annotation<Requirement[]>({
reducer: (prev, next) => [...prev, ...next],
default: () => [],
}),
// Messages for conversation history
messages: Annotation<string[]>({
reducer: (prev, next) => [...prev, ...next],
default: () => [],
}),
});
Reducers: Define how array fields are updated. Without a reducer, arrays are replaced. With a reducer, updates are merged.
Nodes are async functions that:
export async function analyzeNode(state: FeatureStateType): Promise<Partial<FeatureStateType>> {
// Do work
const analysis = await analyzeRepository(state.repoPath);
// Return partial update (only changed fields)
return {
repoAnalysis: analysis,
currentPhase: SdlcLifecycle.Requirements,
};
}
Direct edges: Always go from A to B
graph.addEdge('analyze', 'requirements');
Conditional edges: Choose destination based on state
graph.addConditionalEdges('requirements', (state) => {
if (allRequirementsClear(state)) {
return 'plan';
}
return 'requirements'; // Loop back
});
Tools give agents external capabilities:
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
export const myTool = tool(
async (input) => {
// Tool implementation
return result;
},
{
name: 'my_tool',
description: 'What this tool does',
schema: z.object({
param1: z.string().describe('First parameter'),
param2: z.number().optional(),
}),
}
);
// state.ts
import { Annotation } from '@langchain/langgraph';
export const MyWorkflowState = Annotation.Root({
input: Annotation<string>,
intermediate: Annotation<string | null>,
output: Annotation<string | null>,
});
// nodes/process.node.ts
export async function processNode(state: MyWorkflowStateType) {
const processed = await doSomething(state.input);
return { intermediate: processed };
}
// nodes/finalize.node.ts
export async function finalizeNode(state: MyWorkflowStateType) {
const output = await finalize(state.intermediate);
return { output };
}
// graphs/my-workflow.graph.ts
import { StateGraph, START, END } from '@langchain/langgraph';
import { MyWorkflowState } from '../state';
import { processNode, finalizeNode } from '../nodes';
export function createMyWorkflowGraph() {
return new StateGraph(MyWorkflowState)
.addNode('process', processNode)
.addNode('finalize', finalizeNode)
.addEdge(START, 'process')
.addEdge('process', 'finalize')
.addEdge('finalize', END)
.compile();
}
import { ChatAnthropic } from '@langchain/anthropic';
import { contextQueryTool, fileSystemTool } from '../tools';
const model = new ChatAnthropic({
modelName: 'claude-sonnet-4-20250514',
});
const modelWithTools = model.bindTools([contextQueryTool, fileSystemTool]);
export async function researchNode(state: FeatureStateType) {
const response = await modelWithTools.invoke([
{ role: 'system', content: 'Research the codebase for relevant context.' },
{ role: 'user', content: state.featureDescription },
]);
// Model may have called tools - extract results
const toolCalls = response.tool_calls || [];
return {
messages: [response],
context: extractContext(toolCalls),
};
}
graph.addConditionalEdges('gather', (state) => {
if (isComplete(state)) {
return 'next_step';
}
return 'gather'; // Loop back
});
import { interrupt } from '@langchain/langgraph';
export async function approvalNode(state: FeatureStateType) {
// Pause execution and wait for human approval
const approved = await interrupt({
type: 'approval_required',
data: state.plan,
});
if (!approved) {
throw new Error('Plan rejected');
}
return { approved: true };
}
For complex multi-agent orchestration:
async function supervisorNode(state: SupervisorStateType) {
const response = await model.invoke([
{ role: 'system', content: SUPERVISOR_PROMPT },
...state.messages,
]);
const decision = parseDecision(response.content);
return new Command({
goto: decision.nextAgent,
update: { messages: [response] },
});
}
export function createSupervisorGraph() {
return new StateGraph(SupervisorState)
.addNode('supervisor', supervisorNode)
.addNode('researcher', researcherNode)
.addNode('planner', plannerNode)
.addNode('executor', executorNode)
.addEdge(START, 'supervisor')
.addConditionalEdges('supervisor', (state) => state.nextAgent)
.addEdge('researcher', 'supervisor')
.addEdge('planner', 'supervisor')
.addEdge('executor', 'supervisor')
.compile();
}
// Run multiple nodes in parallel
graph.addNode('parallel_tasks', async (state) => {
const [result1, result2, result3] = await Promise.all([task1(state), task2(state), task3(state)]);
return {
results: [result1, result2, result3],
};
});
// tests/unit/agents/nodes/analyze.node.test.ts
import { describe, it, expect, vi } from 'vitest';
import { analyzeNode } from '@/infrastructure/services/agents/feature-agent/nodes/analyze.node';
describe('analyzeNode', () => {
it('should analyze repository and update state', async () => {
const state = {
repoPath: '/test/repo',
repoAnalysis: null,
currentPhase: SdlcLifecycle.Requirements,
};
const result = await analyzeNode(state);
expect(result.repoAnalysis).toBeDefined();
expect(result.currentPhase).toBe(SdlcLifecycle.Requirements);
});
});
// tests/integration/agents/graphs/feature.graph.test.ts
import { describe, it, expect } from 'vitest';
import { createFeatureGraph } from '@/infrastructure/services/agents/feature-agent/graphs/feature.graph';
describe('FeatureGraph', () => {
it('should complete full workflow', async () => {
const graph = createFeatureGraph();
const result = await graph.invoke({
repoPath: './test-fixtures/sample-repo',
featureDescription: 'Add logging',
});
expect(result.requirements.length).toBeGreaterThan(0);
expect(result.plan).toBeDefined();
expect(result.tasks.length).toBeGreaterThan(0);
});
});
const graph = createFeatureGraph();
// Enable tracing
process.env.LANGCHAIN_TRACING_V2 = 'true';
process.env.LANGCHAIN_API_KEY = 'your-key';
const result = await graph.invoke(input, {
callbacks: [new ConsoleCallbackHandler()],
});
const stream = await graph.stream(input);
for await (const event of stream) {
console.log('=== Node:', event.node, '===');
console.log('State keys:', Object.keys(event.state));
console.log('Messages:', event.state.messages?.length);
}
Each node should do one thing well:
// Good: Single responsibility
export async function validateRequirementsNode(state) {
const validation = validateRequirements(state.requirements);
return { validationResult: validation };
}
// Bad: Too many responsibilities
export async function doEverythingNode(state) {
const validated = validateRequirements(state.requirements);
const plan = createPlan(validated);
const tasks = breakdownTasks(plan);
// ...
}
// Good: Clear branching logic
graph.addConditionalEdges('validate', (state) => {
if (state.validationResult.isValid) return 'plan';
if (state.validationResult.needsClarification) return 'clarify';
return 'error';
});
export async function safeNode(state: FeatureStateType) {
try {
const result = await riskyOperation(state);
return { result, error: null };
} catch (error) {
return {
result: null,
error: error.message,
currentPhase: SdlcLifecycle.Error,
};
}
}
// Define state type
export type FeatureStateType = typeof FeatureState.State;
// Use in nodes
export async function myNode(state: FeatureStateType): Promise<Partial<FeatureStateType>> {
// TypeScript catches invalid state updates
}
Update when:
Related docs: