tion. Beyond 5 iterations, diminishing returns and LLM drift increase latency without accuracy gains.
Core Solution
The architecture implements a ReAct (Reason + Act) loop using LangGraph's StateGraph. Unlike linear chains, LangGraph supports cyclic edges, enabling the agent to autonomously decide whether to continue reasoning, execute an API action, or terminate.
Architecture Decision
- State Schema: Centralized TypeScript interface tracking issue context, LLM reasoning steps, executed actions, and termination flags.
- Tool Binding: Octokit REST client wrapped in LangChain-compatible tools with strict Zod schemas for function calling.
- Conditional Routing: Edges dynamically route between
analyze, decide, execute, and terminate nodes based on LLM output and state validation.
Project Setup & Configuration
Create a new project directory and initialize it with npm. Open your terminal and run the following commands:
mkdir github-issue-agent
cd github-issue-agent
npm init -y
Enter fullscreen mode Exit fullscreen mode
Now install the required dependencies. You'll need LangGraph, LangChain core modules, the OpenAI integration, and the Octokit library for GitHub API access.
npm install @langchain/langgraph @langchain/core @langchain/openai @octokit/rest dotenv zod
Enter fullscreen mode Exit fullscreen mode
Create a .env file in your project root to store your API keys securely:
bash
OPENAI_API_KEY=sk-your-openai-key-here
GITHUB_TOKEN=ghp_your-github-token-here
GITHUB_OWNER=your-github-user
---
π **[Read the full tutorial on AI Tutorials β](https://tutorial.gogoai.xin/tutorial/build-an-ai-agent-for-github-issues-with-langgraph)**
Enter fullscreen mode Exit fullscreen mode
import { Annotation, StateGraph, END } from "@langchain/langgraph";
import { z } from "zod";
import { Octokit } from "@octokit/rest";
// 1. Define persistent state schema
const AgentState = Annotation.Root({
issues: Annotation<any[]>,
currentIssue: Annotation<any>,
reasoning: Annotation<string[]>,
actionTaken: Annotation<string>,
maxIterations: Annotation<number>,
iterationCount: Annotation<number>,
shouldTerminate: Annotation<boolean>,
});
// 2. Initialize Octokit with rate-limit aware configuration
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
request: { retries: 3, backoffStrategy: (retryAfter: number) => retryAfter * 1000 }
});
// 3. Define GitHub API tools with strict Zod schemas
const tools = [
{
name: "label_issue",
description: "Apply labels to a GitHub issue",
schema: z.object({
issue_number: z.number(),
labels: z.array(z.string())
}),
execute: async (args: { issue_number: number; labels: string[] }) => {
await octokit.rest.issues.addLabels({
owner: process.env.GITHUB_OWNER!,
repo: "target-repo",
issue_number: args.issue_number,
labels: args.labels
});
return `Applied labels: ${args.labels.join(", ")}`;
}
},
{
name: "post_comment",
description: "Post a comment on a GitHub issue",
schema: z.object({
issue_number: z.number(),
body: z.string()
}),
execute: async (args: { issue_number: number; body: string }) => {
await octokit.rest.issues.createComment({
owner: process.env.GITHUB_OWNER!,
repo: "target-repo",
issue_number: args.issue_number,
body: args.body
});
return "Comment posted successfully";
}
}
];
Graph Construction & Execution Loop
// 4. Define node functions
const analyzeIssue = async (state: typeof AgentState.State) => {
// LLM reasoning step: parse intent, severity, and required action
const prompt = `Analyze issue #${state.currentIssue.number}: "${state.currentIssue.title}".
Determine severity, required labels, and whether to comment/assign/close.`;
const reasoning = await llm.invoke(prompt);
return { ...state, reasoning: [...state.reasoning, reasoning] };
};
const decideAction = async (state: typeof AgentState.State) => {
// LLM decision routing based on reasoning + state
const decision = await llm.invoke(`Based on: ${state.reasoning.join("\n")},
select action: label_issue, post_comment, or terminate.`);
if (decision.includes("terminate") || state.iterationCount >= state.maxIterations) {
return { ...state, shouldTerminate: true };
}
return { ...state, actionTaken: decision, iterationCount: state.iterationCount + 1 };
};
const executeAction = async (state: typeof AgentState.State) => {
const tool = tools.find(t => t.name === state.actionTaken);
if (!tool) throw new Error(`Unknown action: ${state.actionTaken}`);
const result = await tool.execute(state.currentIssue);
return { ...state, reasoning: [...state.reasoning, `Executed: ${result}`] };
};
// 5. Compile state graph with conditional edges
const graph = new StateGraph(AgentState)
.addNode("analyze", analyzeIssue)
.addNode("decide", decideAction)
.addNode("execute", executeAction)
.addEdge("analyze", "decide")
.addConditionalEdges("decide", (state) => {
return state.shouldTerminate ? "terminate" : "execute";
}, { terminate: END, execute: "execute" })
.addEdge("execute", "analyze") // ReAct loop
.setEntryPoint("analyze")
.compile();
// 6. Runtime execution
const runAgent = async (issues: any[]) => {
for (const issue of issues) {
const initialState = {
issues,
currentIssue: issue,
reasoning: [],
actionTaken: "",
maxIterations: 5,
iterationCount: 0,
shouldTerminate: false
};
const result = await graph.invoke(initialState);
console.log(`Issue #${issue.number} processed. Final state:`, result);
}
};
Pitfall Guide
- State Mutation Side-Effects: LangGraph relies on immutable state snapshots for checkpointing and backtracking. Directly mutating
state.currentIssue or arrays breaks the state machine. Always return new state objects using spread operators or immutable update patterns.
- Unbounded ReAct Loops: LLMs can enter infinite reasoning cycles when feedback is ambiguous. Implement hard iteration limits (
maxIterations), early stopping on confidence thresholds, and explicit terminate routing to prevent runaway API consumption.
- GitHub Secondary Rate Limiting: The GitHub API enforces strict secondary rate limits on mutation endpoints (
POST/PATCH). Unthrottled parallel agent executions will trigger 403 Forbidden. Implement exponential backoff with jitter and respect Retry-After headers in the Octokit configuration.
- Tool Schema Mismatch: Zod schemas must strictly align with the LLM's function-calling JSON schema. Loose typing (e.g.,
z.any() or missing required fields) causes argument hallucination and runtime crashes. Always validate tool inputs against the exact API payload structure.
- Context Window Overflow: Feeding full issue threads, PR links, and commit history exceeds LLM context limits, degrading reasoning quality. Apply semantic truncation, extract only titles/body/labels, or use embedding-based retrieval to inject only relevant context.
- Missing Conditional Edge Fallbacks: If the LLM returns an action not mapped in
addConditionalEdges, the graph throws a routing error. Always define a default fallback node or sanitize LLM output through a response parser before edge evaluation.
- Token Leakage in Production: Hardcoding
.env variables in version control or logging full LLM prompts/response cycles exposes API keys. Use secret managers, redact sensitive fields in state snapshots, and disable verbose logging in production deployments.
Deliverables
- π LangGraph State Machine Blueprint: Visual architecture diagram detailing node dependencies, conditional routing logic, checkpointing strategy, and ReAct loop termination conditions. Includes TypeScript type definitions and state transition matrix.
- β
Pre-Flight & Deployment Checklist: 24-point validation covering API scope verification (
repo, read:org), rate limit configuration, Zod schema validation, loop safety bounds, environment variable encryption, and local vs. cloud execution parity.
- βοΈ Configuration Templates: Production-ready
.env scaffolding, tsconfig.json with strict mode & ESM compatibility, langgraph.json for cloud deployment, Dockerfile with multi-stage build optimization, and GitHub Actions CI workflow for automated agent testing against mock repositories.