Last week, we had a hackathon at work where we were encouraged to build with Cursor to learn and get more proficient with it. In this note, I share my vibe coded multi-agent travel itinerary planner which you can find on Github. In the process, I learned a bit of LangGraph, Google Places API, and Streamlit. Let’s go!

Why Would Anyone Use This?
This toy project was definitely for learning purposes. I’m wary of using AI for all things, as they can suck the joy and any sense of accomplishment sometimes.
Maybe there’s a place in the world for this, though. I feel there is merit to automating the parts of travel planning that can really be a slog. There’s a lot of information out there on the internet, not to mention differing opinions in a group dynamic to process, before anything crystallizes into decision. It’s difficult to come up with an objective approach when preferences don’t not always align.
Preserving Authentic Experiences
It’s important to acknowledge that over-planning is exactly how you’d ruin any shot at experiencing authentic genuine moments. Leave room for serendipity. In other words— perfect! Because this little project is going to be far from perfect.
Using Claude Sonnet
For this project, I used Claude Sonnet 4. What I ultimately came up with after some back and forth with Cursor, was a multi-agent travel itinerary planner in Python using Pydantic, LangGraph, Google Places API and Streamlit. I did tell Cursor specifically that I wanted to learn this stack. With that, here’s a basic recap of my learnings about multi-agent systems:
Technical 1 2
LangGraph is a framework designed for building complex AI applications using state machines and graph structures.
State: A shared data structure that holds information about the current status of the agent.
Transitions: Define how the agent moves from one state to another based on specific conditions or inputs
Node: Nodes represent discrete processing steps within the graph. Each node can be thought of as a function that performs a specific task or computation. A node becomes active when it receives a new message (state) through any of its incoming edges. Upon activation, the node executes its associated function.
Edge: Edges are the connections between nodes that define the flow of execution. They dictate how data moves from one node to another. Edges facilitate communication between nodes, allowing them to pass states and trigger subsequent actions.
Schema: Schemas outline the expected format for the state data, which can be represented using:
- TypedDict: A built-in Python type for defining dictionaries with specific key-value types.
- Pydantic Models: A library that provides data validation and settings management using Python type annotations (what we’ll use)
Core Components
State Machine (LangGraph)
A StateGraph acts as a state machine, and manages all the agent nodes:
# StateGraph coordinates which agent runs, handles errors and retries, and tracks progress.
workflow = StateGraph(TravelState)
workflow.add_node("research", research_agent.execute)
workflow.add_node("budget", budget_planning_node)
# ... more agents
return workflow.compile()
Shared State System
TravelState is a shared mutable state that agents can read and write to:
class TravelState(BaseModel):
preferences: TravelPreferences # What the user wants
destinations: List[Destination] # Where they're going
all_activities: List[Activity] # Things to do
itinerary: List[DayItinerary] # Day-by-day plans
budget_breakdown: BudgetBreakdown # How money is allocated
# ... more state
Each agent works with the same TravelState state to ensure consistency. This state allows agents to see what changes are made by others. Here are two examples:
Google Places Agent
An agent does one job. The Google Places agent connects to Places data with the Google places provider to pull down real attraction coordinates, names, locations, business hours, and ratings.
def _search_real_destinations(self, query: str):
# Makes actual API calls to Google Places
response = requests.post(
"https://places.googleapis.com/v1/places:searchText",
headers={"X-Goog-Api-Key": self.api_key},
json={"textQuery": f"{query} city", "maxResultCount": 3}
)
Research Agent
The research agent’s job is to find destinations and activities using real data.
def execute(self, state: TravelState) -> TravelState:
for destination_name in state.preferences.destinations:
# Find the actual place (e.g. Tokyo)
search_results = self.search_destinations_tool(destination_name)
# Find activities there
activities = self.find_activities_tool(destination_name, interests)
# Add to shared state
state.destinations.append(destination_object)
state.all_activities.extend(activity_objects)
Schedule Agent
The schedule agent creates day-by-day plans.
for day in range(destination.days_allocated):
day_activities = []
# Smart activity selection - no repeats across entire trip
for activity in available_activities[:3]: # Max 3 per day
if activity.name not in used_activity_names:
day_activities.append(activity)
used_activity_names.add(activity.name)
# Calculate actual daily costs
activity_costs = sum(activity.cost for activity in day_activities)
daily_total = accommodation_cost + food_cost + activity_costs
Web Interface(Sreamlit)
The Streamlit interface takes user input. Straight forward.
destinations_input = st.text_area("Enter destinations...")
budget = st.number_input("Total Budget (USD)", min_value=500)
interests = st.multiselect("What interests you?", ["cultural", "food", ...])
Conclusion
Coming out of this, I would say that I’m a little better at understanding multi agent systems. Hurray! And I no longer scoff at AI travel itinerary generators out there— they have their place.
It was interesting to build agents and piece them together literally like Lego. In contrast totrip planning done by people, the agents work better when they deal with individual responsibilities, even when they are interdependent factors inherent (research, budget, schedule, dining, transport, etc.) to travel.
I find the stickiest part in trip planning is arriving at a high-level understanding of the desired trip outcome, and I feel okay with automating this. Like most things, without a high-level understanding, it’s easy to become hyper-focused on a detail and neglect the greater picture, creating wasted effort. Ideas and inspiration don’t always make it into the final timeline as new considerations are added and evaluated. Any reduction of friction here would be a win.
Some things that Cursor didn’t initially consider, were the deduplication of activities across the entire trip, balancing of costs, travel time considerations between locations, and energy level.