惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News | PayPal Newsroom
P
Proofpoint News Feed
Cyberwarzone
Cyberwarzone
C
Cisco Blogs
SecWiki News
SecWiki News
Know Your Adversary
Know Your Adversary
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Vercel News
Vercel News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
NISL@THU
NISL@THU
WordPress大学
WordPress大学
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Threat Research - Cisco Blogs
AI
AI
Simon Willison's Weblog
Simon Willison's Weblog
Security Archives - TechRepublic
Security Archives - TechRepublic
有赞技术团队
有赞技术团队
L
LINUX DO - 热门话题
Hacker News: Ask HN
Hacker News: Ask HN
V
V2EX
G
GRAHAM CLULEY
TaoSecurity Blog
TaoSecurity Blog
Hugging Face - Blog
Hugging Face - Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
F
Fortinet All Blogs
博客园 - 叶小钗
博客园 - 三生石上(FineUI控件)
云风的 BLOG
云风的 BLOG
Recorded Future
Recorded Future
Latest news
Latest news
The Hacker News
The Hacker News
aimingoo的专栏
aimingoo的专栏
T
Troy Hunt's Blog
S
Schneier on Security
I
Intezer
Google DeepMind News
Google DeepMind News
A
Arctic Wolf
Apple Machine Learning Research
Apple Machine Learning Research
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threatpost
爱范儿
爱范儿
The Register - Security
The Register - Security
S
SegmentFault 最新的问题
Blog — PlanetScale
Blog — PlanetScale
博客园 - 聂微东
宝玉的分享
宝玉的分享
Recent Commits to openclaw:main
Recent Commits to openclaw:main
美团技术团队
B
Blog RSS Feed

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Building a Conversational Booking Agent for Vehicle Rentals: MCP Endpoints, Dialog Passports, and Alternative Search
Dmitry Stepa · 2026-05-20 · via DEV Community

Building a Conversational Booking Agent for Vehicle Rentals: MCP Endpoints, Dialog Passports, and Alternative Search

When we built WorCo — a CRM for vehicle rental businesses — the AI booking agent was supposed to be a side feature. It became the core of the product.

This is the technical breakdown of how our agent actually works: how we designed the MCP-compatible API layer, how we solved the "AI agents forget things mid-conversation" problem using Redis-backed dialog passports, and what we learned from letting an LLM call real booking endpoints.


The problem with messaging-first rental businesses

Small rental operators in Southeast Asia — motorcycles in Phuket, scooters in Bali, boats in Koh Samui — run their entire sales funnel through WhatsApp and Telegram. A customer messages, asks about availability, negotiates dates and price, and either books or ghosts.

For the operator, every conversation is manual. For the customer, it's slow.

Our system connects directly to these messaging channels. The AI agent reads the incoming message, searches the fleet, calculates seasonal pricing, and creates a real booking record — without a human in the loop. When it works well, the booking appears in the operator's Kanban board within a few seconds of the customer typing "I want to rent a motorcycle next week."


Architecture: three distinct layers

Telegram / WhatsApp (inbound messages)
        │
        ▼
OpenAI Agents SDK (bot_image/openai_agents/)
  ├── Agent with @function_tool decorated tools
  ├── CRMClient — synchronous HTTP wrapper
  └── Redis-backed Dialog Passport
        │
        │  X-Bot-Token authenticated HTTP
        ▼
Django REST API (/api/mcp/ endpoints)
        │
        ├── Vehicle availability + conflict detection
        ├── Seasonal pricing calculation
        ├── Booking creation with idempotency
        └── Client CRM (auto-create + merge)

Enter fullscreen mode Exit fullscreen mode

The key architectural decision: the AI layer (OpenAI Agents SDK) and the business logic layer (Django) are completely separate containers. The agent calls HTTP endpoints the same way any external system would. There's no shared process, no direct ORM access from the agent side.


The MCP API layer

We chose not to run a formal MCP protocol server. Instead, we built REST endpoints that follow MCP's tool-call semantics — each endpoint has a single clear purpose, takes structured input, and returns structured output. Any agent that can make HTTP calls can use them.

All endpoints live under /api/mcp/ and require an X-Bot-Token header. This token identifies the bot and its associated company — all queries are automatically scoped to that company's data.

# modules/communications/views/mcp.py

def authenticate_bot(request) -> tuple[bool, Bot, str]:
    bot_token = request.headers.get('X-Bot-Token')
    if not bot_token:
        return False, None, "Missing X-Bot-Token header"

    try:
        bot = Bot.objects.get(token=bot_token, is_active=True)
        return True, bot, ""
    except Bot.DoesNotExist:
        return False, None, "Invalid or inactive bot token"

Enter fullscreen mode Exit fullscreen mode

Every view starts with authenticate_bot(). The company context flows automatically from the bot record — the agent never explicitly passes a company ID.

The tools we expose

GET  /api/mcp/vehicles/search_available/   # fleet search with filters
GET  /api/mcp/vehicles/availability/        # check a specific vehicle
GET  /api/mcp/vehicles/pricing/             # pricing tiers for a vehicle
GET  /api/mcp/vehicles/{uuid}/bookings/     # existing reservations
POST /api/mcp/vehicles/rentals/             # create a booking
GET  /api/mcp/company_info/                 # company details, currency
GET  /api/mcp/offices/                      # office locations
GET  /api/mcp/client/info/                  # client lookup by Telegram ID
POST /api/mcp/client/update/                # update client profile
GET  /api/mcp/health/                       # liveness check

Enter fullscreen mode Exit fullscreen mode


Vehicle search with tokenized matching

The search endpoint (/api/mcp/vehicles/search_available/) is the most-called tool in a typical booking conversation. We put significant effort into making it forgiving — customers don't say "Honda PCX 150", they say "small scooter" or "honda something automatic".

The brand/model filter uses AND-over-tokens logic: we split the input into tokens by non-alphanumeric characters, then require each token to match across multiple fields.

# modules/communications/views/mcp.py

import re as _re
tokens: list[str] = []
for part in [brand or "", model or ""]:
    if part:
        for t in _re.split(r"[^\w]+", part, flags=_re.IGNORECASE):
            t_norm = (t or "").strip().lower()
            if len(t_norm) >= 2:
                tokens.append(t_norm)

if tokens:
    for t in tokens:
        tok_filter = (
            Q(brand__id__icontains=t) |
            Q(brand__translations__name__icontains=t) |
            Q(model__slug__icontains=t) |
            Q(model__translations__name__icontains=t) |
            Q(custom_model_details__icontains=t) |
            Q(license_plate__icontains=t)
        )
        vehicles_qs = vehicles_qs.filter(tok_filter).distinct()

Enter fullscreen mode Exit fullscreen mode

"BMW GS 1200" becomes tokens ["bmw", "gs", "1200"]. Each token is ANDed — a vehicle must match all of them — but each token searches across brand slug, translated brand name, model slug, translated model name, custom details, and license plate.

Availability filtering uses strict interval overlap: a vehicle is excluded if there's any rental with status in ['pending', 'confirmed', 'active'] where rental.start_date < requested_end AND rental.end_date > requested_start.

conflicting = VehicleRental.objects.filter(
    vehicle__in=vehicles_qs,
    start_date__lt=end_dt,
    end_date__gt=start_dt,
    status__in=['pending', 'confirmed', 'active']
).values_list('vehicle__uuid', flat=True)

available = vehicles_qs.exclude(uuid__in=list(conflicting))

Enter fullscreen mode Exit fullscreen mode


Alternative period search

Here's a problem we didn't anticipate: an AI agent that just says "sorry, not available" is useless. We built a three-strategy alternative search that the agent calls automatically when the requested period is blocked.

# bot_image/openai_agents/tools.py

def _find_alternative_periods(
    vehicle_uuid: str,
    vehicle_info: Dict[str, Any],
    start_date: date,
    end_date: date,
    booked_ranges: List[Tuple[date, date]],
    max_results: int = 3
) -> List[Dict[str, Any]]:
    """
    Strategy 1: Period BEFORE first conflict
      - Keep the same start_date, end before the conflict begins
    Strategy 2: Period AFTER last conflict
      - Start from conflict end, maintain same duration
    Strategy 3: Gaps BETWEEN multiple bookings
      - Find free windows between existing reservations
    """

Enter fullscreen mode Exit fullscreen mode

The agent fetches existing bookings via /api/mcp/vehicles/{uuid}/bookings/, runs conflict detection locally, then presents alternatives with pre-calculated pricing. The customer sees something like: "June 10–17 is taken, but I can offer June 3–10 or June 20–27 at the same rate."

We never move both boundaries simultaneously — the alternative must be anchored either to the requested start date or to the post-conflict date, not floating freely.


The dialog passport problem

Multi-turn booking conversations have a fundamental state problem. The AI agent is stateless by design — each message is processed independently. But a booking conversation spans multiple turns:

Turn 1: "I want a motorcycle June 10-17"
  → agent searches, finds Honda CB500F, quotes price

Turn 2: "Yes, the Honda. My name is Alex"
  → agent needs to remember: which vehicle? which dates?

Turn 3: "+66 812 345 678"
  → agent needs: vehicle, dates, client name to create booking

Enter fullscreen mode Exit fullscreen mode

Without persistent state, the agent would need to re-call search endpoints on every turn, or rely on the LLM to extract context from conversation history — unreliable and expensive.

Our solution: a dialog passport stored in Redis, keyed by chat_id.

# bot_image/openai_agents/redis_store.py

def _passport_key(chat_id: str) -> str:
    return f"passport:{chat_id}"

def set_passport(chat_id: str, passport: Dict[str, Any], ttl_seconds: Optional[int] = None) -> bool:
    r = get_redis()
    key = _passport_key(chat_id)
    payload = json.dumps(passport, ensure_ascii=False)
    if ttl_seconds and ttl_seconds > 0:
        r.setex(key, ttl_seconds, payload)
    else:
        r.set(key, payload)
    return True

def get_passport(chat_id: str) -> Optional[Dict[str, Any]]:
    r = get_redis()
    data = r.get(_passport_key(chat_id))
    return json.loads(data) if data else None

Enter fullscreen mode Exit fullscreen mode

The passport has two sections:

{
  "client": {
    "first_name": "Alex",
    "contacts": [
      {"type": "phone", "value": "+66812345678"},
      {"type": "telegram_id", "value": "123456789"}
    ]
  },
  "rental_context": {
    "vehicle_uuid": "abc-123-...",
    "start_date": "2026-06-10",
    "end_date": "2026-06-17",
    "total_price": 280.0
  }
}

Enter fullscreen mode Exit fullscreen mode

The agent writes to the passport as it collects information. The create_vehicle_rental tool reads it when building the booking payload:

# bot_image/openai_agents/tools.py

def _collect_booking_context(conv_id, explicit_data):
    ctx_data = {**explicit_data}

    if conv_id:
        chat_id = _conv_id_to_chat_id(conv_id)
        passport = rs.get_passport(chat_id) or {}
        ctx_client = passport.get('client') or {}
        ctx_rental = passport.get('rental_context') or {}

        ctx_data['client'] = ctx_client
        ctx_data['rental'] = dict(ctx_rental)

    return ctx_data

Enter fullscreen mode Exit fullscreen mode

The passport has a configurable TTL — typically a few hours. If the conversation goes cold, the next message from that customer starts fresh.

We also store secondary data in Redis under the same chat_id prefix:

  • search:{chat_id} — last search results (so the agent can reference "the second option" without re-querying)
  • last_uuid:{chat_id} — UUID of the vehicle currently in discussion
  • status:{chat_id} — conversation phase (searching / quoting / confirming / booked)
  • history:{chat_id} — trimmed message history (max 50 items, FIFO)

Booking creation with idempotency

The POST /api/mcp/vehicles/rentals/ endpoint creates bookings. The critical concern: an LLM might call this endpoint twice for the same booking — once from a retry, once from confused reasoning about whether the booking succeeded.

We handle this with an explicit idempotency_key:

# modules/communications/views/mcp.py

if idempotency_key:
    existing = VehicleRental.objects.filter(
        company=company,
        vehicle=vehicle,
        client__first_name=client_first_name,
        start_date=start_date,
        end_date=end_date,
        extra_data__idempotency_key=idempotency_key
    ).first()
    if existing:
        return JsonResponse({
            'rental_uuid': str(existing.uuid),
            'order_number': existing.order_number,
            'status': existing.status,
            'idempotent': True  # signals this was a duplicate call
        })

Enter fullscreen mode Exit fullscreen mode

The idempotency_key is stored in extra_data (a JSONField on the rental model). The agent generates a key from the conversation context — typically f"{chat_id}:{vehicle_uuid}:{start_date}:{end_date}". If the booking already exists with that key, we return the existing record with idempotent: True so the agent knows it's a repeat.

Conflict detection runs before creation:

conflicting_rentals = VehicleRental.objects.filter(
    vehicle=vehicle,
    start_date__lt=end_dt,
    end_date__gt=start_dt,
    status__in=['pending', 'confirmed', 'active']
)

if conflicting_rentals.exists():
    return JsonResponse({
        'error': 'Vehicle unavailable for requested dates',
        'conflicting_rentals': [
            {'start_date': r.start_date.isoformat(), 'end_date': r.end_date.isoformat(), 'status': r.status}
            for r in conflicting_rentals
        ]
    }, status=400)

Enter fullscreen mode Exit fullscreen mode

The endpoint returns the conflicting rentals so the agent can suggest alternatives without an additional round-trip.


Client auto-create and merge

Customers messaging through Telegram don't have a CRM record yet. The booking endpoint auto-creates clients and handles the merge problem when a placeholder client gets real contact details later.

The flow:

  1. First message arrives from Telegram user 123456789 — a placeholder client is created with their Telegram ID as the only contact
  2. Customer provides their phone number — the passport is updated
  3. On booking creation, we look up existing clients by phone, then by Telegram ID
  4. If we find a placeholder (auto-created, no name), we merge it with the incoming data
  5. If we find a real client, we link the booking to them and update any missing contacts

This means a customer who messaged three months ago and gave their phone number is automatically recognized when they message again.


The OpenAI Agents SDK side

The agent tools are defined using @function_tool from the OpenAI Agents SDK. Each tool maps one-to-one with an MCP endpoint:

# bot_image/openai_agents/tools.py

from agents import function_tool as _function_tool

@tool
def search_available_vehicles(
    start_date: str,
    end_date: str,
    brand: Optional[str] = None,
    model: Optional[str] = None,
    budget: Optional[float] = None,
    office_id: Optional[str] = None,
) -> str:
    """
    Search for available vehicles in the fleet for given dates.
    Returns vehicles with pricing. If requested dates are unavailable,
    also returns alternative available periods.
    """
    client = _get_crm_client()
    response = client.search_available_vehicles(
        start_date=start_date,
        end_date=end_date,
        brand=brand,
        model=model,
        budget=budget,
        office_id=office_id,
    )
    # ... format and return

Enter fullscreen mode Exit fullscreen mode

The CRMClient is a plain requests-based synchronous HTTP client. It carries the X-Bot-Token header and optional Telegram context headers (X-User-Id, X-User-Username) that allow the Django side to log which Telegram user triggered each action.

Per-request client instances are injected via runtime_context — a module-level object that the agent handler sets at the start of each request. This avoids shared mutable state between concurrent conversations.


What we got wrong (and fixed)

Tool descriptions matter more than you think. Early versions of our search_available_vehicles tool had a vague description. The agent would call it with brand="motorcycle" (a vehicle type, not a brand) and get confused by empty results. Rewriting the description to explicitly distinguish vehicle type from brand — with examples — fixed this class of error without any code changes.

AI agents retry aggressively on ambiguity. When the availability check returned a 400 with conflict details, the agent sometimes called the endpoint again with slightly different dates, assuming the first call failed. Adding idempotent: True to successful duplicate calls and conflicting_rentals to conflict responses gave the agent enough signal to stop retrying.

State across turns requires explicit design. We initially relied on the LLM's context window to remember vehicle UUIDs and dates. It worked 80% of the time. The passport system pushed this to near-100% by making state explicit and retrievable rather than implicit and reconstructed.

Pricing must never be client-side. Our pricing model has tiered day-range rates plus seasonal rules with year-boundary wraparound (a Nov–Mar high season rule is not a simple date range). Any client-side calculation will be wrong for edge cases. The get_pricing tool always calls the server and returns a single pre-calculated number.


What's running in production

The full stack in Docker Compose:

  • Django 5 (backend + MCP endpoints)
  • OpenAI Agents SDK (bot container, one per multi-tenant deployment)
  • Redis (dialog passports, search cache, conversation history)
  • PostgreSQL 15 (all business data)
  • Telegram Bot API (inbound/outbound messages)

Orders created by the AI agent are tagged source='mcp' in the VehicleRental model — the same source-tagging pattern we use for admin-panel bookings, online widget bookings, and mobile app bookings. This makes it straightforward to track AI-attributed conversions in the operator's analytics.

The Kanban board shows all orders in real-time regardless of source. When the agent creates a booking, it appears on the board immediately and triggers a staff notification via Telegram — same as a manually created order.


The broader pattern

The approach here — REST endpoints that follow MCP semantics, a Redis-backed dialog state store, and tool-decorated functions in the agent SDK — is not specific to vehicle rentals. Any CRM or booking system with:

  • Multi-turn conversational intake (name, contact, dates, preferences)
  • External AI agents that need to read and write business data
  • Real-time availability that changes between turns
  • Idempotency requirements on write operations

...will hit the same design problems. The passport pattern and the MCP-compatible endpoint structure are our solutions. They're not the only solutions, but they've held up across thousands of real conversations.


If you're building something similar or want to talk about AI agents in operational business systems, find us at worco.io or leave a comment.


WorCo is an AI-powered CRM for vehicle rental businesses. Built on Django 5, React 18, TypeScript, PostgreSQL 15, and the OpenAI Agents SDK.