This document explains the complete execution flow of your AI Security & Quality Scanner. It breaks down how LangChain and LangGraph work together to orchestrate multiple AI agents, manage shared memory, and execute tools.
In LangGraph, your application is a State Machine.
- Nodes represent the agents or Python functions doing the work.
- Edges define the flow of execution from one node to the next.
- State is the shared memory that gets passed along the edges to every node.
Agents do not "talk" to each other directly like humans in a chatroom. Instead, they communicate by reading from and writing to a shared state object.
from typing import TypedDict
from pydantic import BaseModel
class Agents(BaseModel):
security_agent: bool
logic_agent: bool
code_quality_agent: bool
class AgentState(TypedDict):
user_query: str # Written by Chat node
project_path: str # Written by CLI
agents_required: Agents # Written by Planner node
project_map: str # Written by Scanner node
plan: str # Written by Planner node
security_report: str # Written by Security agent
logic_report: str # Written by Logic agent
quality_report: str # Written by Code Quality agent
final_report: str # Written by Report nodeHow State Updates Work:
When a node finishes, it returns a Python dictionary. LangGraph takes that dictionary and merges it into the global state.
For example, if the state is currently {"project_path": "/my/app"} and the Scanner node returns {"project_map": "file1.py, file2.py"}, the new state becomes:
{"project_path": "/my/app", "project_map": "file1.py, file2.py"}.
Here is the exact journey of a single user request through your system:
- The user runs
python cli.py. - The user chats with the
chat_agentto clarify their problem. - The user provides the
project_path. - The CLI packages this into the initial state and starts the graph:
app_graph.stream({"user_query": query, "project_path": path})
The graph starts at START and flows into the chat node.
def chat(state: AgentState):
# Reads: state["user_query"] (The raw chat history)
# Action: Asks an LLM to summarize the history into a concise goal.
# Writes: state["user_query"] (Overwrites it with the clean summary)
return {"user_query": clean_summary}The graph flows from chat to scanner.
def project_scanner_node(state: AgentState):
# Reads: state["user_query"] and state["project_path"]
# Action: The scanner agent looks at the codebase to understand the structure.
# Writes: state["project_map"]
return {"project_map": result}The graph flows from scanner to planner. This is where the magic happens.
- The
planner_agentreads theproject_mapand writes a step-by-stepplan. - We use
.with_structured_output(Agents)to force an LLM to read that plan and flip boolean switches for which agents are actually needed.
def planner_node(state: AgentState):
# Reads: state["user_query"] and state["project_map"]
# Writes: state["plan"] and state["agents_required"]
return {"plan": plan_text, "agents_required": agents_required_object}The Conditional Edge:
Instead of a normal edge, the planner uses a conditional edge. LangGraph calls the route_to_agents function:
def route_to_agents(state: AgentState):
destinations = []
# Reads the boolean flags saved by the planner
if state["agents_required"].security_agent:
destinations.append("security")
# ... checks logic and code_quality ...
return destinations # LangGraph routes to these nodes in PARALLELIf the planner requested security and logic, LangGraph spins them up at the same time.
- Security Node reads the
project_mapandplan, hunts for vulnerabilities, and returns{"security_report": "..."} - Logic Node reads the
project_mapandplan, looks for business logic bugs, and returns{"logic_report": "..."}
Once all active specialist agents finish, the graph converges on the report node.
def report_node(state: AgentState):
# Reads: ALL the reports generated by the specialists
# Action: Combines them into one giant markdown string
# Writes: state["final_report"]
return {"final_report": report_text}The graph hits END, and the CLI prints the final_report to the screen.
Inside the nodes, you invoke actual Agents (like security_agent). But what exactly is an Agent?
An Agent is an LLM that has been given a system_prompt and a list of tools.
security_agent = create_agent(
model="google_genai:gemini-3.1-flash-lite",
tools=[run_terminal],
system_prompt="You are a Security Agent..."
)When you call security_agent.invoke(...), the LLM doesn't just spit out text immediately. It enters a ReAct (Reason + Act) loop:
- Think: The LLM reads the user prompt and its system prompt. It realizes it needs to search the codebase for API keys.
- Act (Tool Call): The LLM outputs a JSON command saying: "Execute
run_terminalwith the argumentgrep -r 'API_KEY' ." - Execute: The LangChain
AgentExecutorcatches this request, pauses the LLM, runs your Pythonrun_terminalfunction, and captures the terminal output. - Observe: The
AgentExecutorfeeds the terminal output back into the LLM as a new message. - Repeat: The LLM reads the output. If it needs more information, it calls another tool. If it has enough information, it writes the final security report.
This all happens invisibly inside the result = security_agent.invoke(...) call in your nodes!
- LangGraph acts as the manager. It holds the shared state and decides which Python function (node) runs next based on the edges.
- Python Nodes act as the wrappers. They take the current state, package it into a prompt, and hand it to an Agent.
- LangChain Agents do the heavy lifting. They receive the prompt, use Tools to interact with the real world (running terminal commands, reading files), and return an answer.
- The wrapper Node takes that answer and updates the State, passing the baton to the next node in the graph.