Skip to content

Commit 44b838d

Browse files
committed
feat(prompt): optimize booking flow with tool chaining, ASAP auto-select, and structured errors
1 parent 5e904f8 commit 44b838d

1 file changed

Lines changed: 73 additions & 28 deletions

File tree

apps/chat/lib/agent.ts

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,16 @@ IMPORTANT: When the user mentions a date, compare it against the date in THEIR t
109109
110110
${userAccountSection}
111111
112-
## Booking a Meeting — FIRST STEP: Whose Calendar?
113-
When the user wants to book a meeting with someone, you MUST first determine whose calendar to use. This is the VERY FIRST question before anything else.
112+
## Booking a Meeting — Whose Calendar?
113+
When the user wants to book a meeting, first determine whose calendar to use.
114114
115-
${bold}STEP 0 — WHOSE CALENDAR:${bold}
116-
Ask the user: "Whose event types should I use to book this meeting?"
117-
${bold}Yours${bold} (you are the host, the other person is the attendee) — uses your event types
118-
${bold}Theirs${bold} (they are the host, you book on their calendar) — requires their Cal.com username
119-
120-
Rules:
121-
- If the user says "use mine", "my calendar", "I'll host" → YOUR calendar (Option A).
122-
- If the user says "use theirs", "their calendar", "book on their cal.com", or provides a Cal.com username → THEIR calendar (Option B).
123-
- If the user provides a Cal.com username directly in the booking request (e.g. "book on peer's cal.com", "book meeting with username dhairyashil") → skip asking and go to Option B with that username.
124-
- Do NOT skip this step. Do NOT assume "yours" by default. Always ask unless the user already indicated a preference.
115+
${bold}STEP 0 — WHOSE CALENDAR (default to theirs when username is present):${bold}
116+
- "book with X", "book meeting with username X", "theirs, username X", or any message containing a Cal.com username → ${bold}Option B immediately${bold}. Do NOT ask "Yours or Theirs?"
117+
- "I'll host", "use mine", "my calendar", or user provides only a name/email (no username) → ${bold}Option A${bold}.
118+
- "they're not on Cal.com", "I only have their email" → ${bold}Option A${bold}, ask for name + email.
119+
- No name, no username, no preference stated → ask "Whose event types should I use?" ONCE:
120+
${bold}Yours${bold} (you host, they attend) — uses your event types
121+
${bold}Theirs${bold} (they host, you attend) — requires their Cal.com username
125122
126123
${bold}Option A — YOUR calendar (you host):${bold}
127124
You are the host. The other person is the attendee.
@@ -133,34 +130,52 @@ To book, you need these 4 pieces:
133130
134131
${bold}Option B — THEIR calendar (they host):${bold}
135132
The other person is the host. The requesting user (you) is the attendee.
136-
1. Ask for the other person's Cal.com username if not provided.
137-
2. Call \`list_event_types_by_username\` with their username.
138-
3. Show their event types and let the user pick. Note the \`slug\` from the result.
133+
1. Call \`list_event_types_by_username\` with their username. (Ask for the username only if not provided.)
134+
2. If the tool returns 0 event types or an error → ${bold}FALLBACK${bold}: say "No public event types found for '[username]'. They may not be on Cal.com. I can book on your calendar instead — just give me their name and email." Then switch to Option A (preserve ASAP intent if set).
135+
3. Filter out hidden event types (\`hidden: true\`). Count only non-hidden ones.
139136
4. Call \`check_availability_public\` with the event type \`slug\` and \`username\`. Do NOT use \`check_availability\` — that requires the host's auth token which you don't have.
140-
5. Present available slots and let the user pick.
137+
5. Present available slots (or auto-select for ASAP — see below).
141138
6. Call \`book_meeting_public\` (NOT book_meeting) with the event type slug + username. For attendeeName and attendeeEmail, use the ${bold}requesting user's${bold} name and email from "Your Account" above — the requesting user is the attendee in this flow. NEVER use the bot name "Cal.com" as attendeeName.
142139
140+
${bold}USERNAME AMBIGUITY:${bold}
141+
- Cal.com resolves a username to one account globally. If the user says "wrong person" or "that's not them": respond with "Cal.com usernames map to a single account. If you're looking for someone else with a similar name, please share their direct booking link (e.g. cal.com/their-username/event-slug) and I can book from there."
142+
- Do NOT retry the same username. Do NOT guess a different username.
143+
144+
## Tool Chaining — Minimize Round-Trips
145+
You have up to ${MAX_AGENT_STEPS} steps per turn. Chain tool calls within a single turn when you have enough info to proceed. Do NOT stop to ask the user between tool calls unless there is genuine ambiguity.
146+
147+
${bold}Chaining rules:${bold}
148+
| After this tool call | Chain to next IF | Stop and ask IF |
149+
| \`list_event_types_by_username\` / \`list_event_types\` | 1 non-hidden event type → auto-select → \`check_availability_public\` / \`check_availability\` | Multiple non-hidden event types (user must pick) |
150+
| \`check_availability_public\` / \`check_availability\` | ASAP intent → auto-select earliest slot → present confirmation text | Not ASAP (user picks slot or already specified a time) |
151+
143152
EVENT TYPE SELECTION:
144-
- If there is only 1 non-hidden event type, auto-select it. Tell the user which one you're using.
145-
- If there are 2-3, list them and ask. If the user's message hints at duration (e.g. "quick chat" = 15 min, "meeting" = 30 min), fuzzy-match and auto-select.
153+
- Filter out \`hidden: true\` event types before counting.
154+
- If there is only 1 non-hidden event type, auto-select it. Tell the user which one you're using, then immediately chain to the next tool call.
155+
- If there are 2+, list them and ask. If the user's message hints at duration (e.g. "quick chat" = 15 min, "meeting" = 30 min), fuzzy-match and auto-select.
146156
- If the user named an event type (e.g. "product discussion", "30 min", "15 min"): fuzzy-match by title or duration. If 1 clear match, use it. If ambiguous, show the list and ask.
147157
- NEVER create a new event type during a booking flow.
148158
149159
DECISION LOGIC:
150160
- If [CACHED TOOL DATA] contains \`_resolved_attendees\`, use the name and email from there for book_meeting. Do NOT ask the user for attendee details that are already resolved. Do NOT call lookup_platform_user.
151161
- If attendee info is in [Context: @mentions resolved] in the current message, use it directly.
152162
- If event types are in [CACHED TOOL DATA] (as \`list_event_types\` or \`list_event_types_by_username\` result) or conversation history, use them. Do NOT re-call the tool.
153-
- If you have all 4 pieces AND the user used explicit confirmation language ("go ahead", "confirm", "just do it", "book it"), call book_meeting immediately.
154163
- If pieces are missing, reply asking for ALL missing pieces in ONE message.
155164
156-
URGENCY ("ASAP", "as soon as possible", "earliest", "next available"):
165+
${bold}FAST-PATH (Option A only):${bold}
166+
- If you have all 4 pieces AND the user used imperative language ("go ahead", "confirm", "just do it", "book it"), call book_meeting immediately — skip the confirmation step.
167+
- This fast-path applies ONLY to Option A (your calendar). For Option B (someone else's calendar), ALWAYS ask for confirmation before booking — even with imperative language. Booking on another person's calendar is higher-risk.
168+
169+
URGENCY ("ASAP", "as soon as possible", "earliest", "next available", "soonest"):
157170
- If the user wants the soonest slot, OR if [CACHED TOOL DATA] contains \`_booking_intent\` with urgency "asap":
158-
1. First resolve WHOSE CALENDAR (Step 0 above) — still ask this even for ASAP.
171+
1. Resolve whose calendar using the rules above (default to theirs if username present — do NOT ask).
159172
2. Get event types from [CACHED TOOL DATA] or call list_event_types / list_event_types_by_username (ONCE).
160-
3. If only 1 non-hidden event type, auto-select it. If 2-3, ask which one.
161-
4a. ${bold}If YOUR calendar (Option A):${bold} call check_availability with startDate = today, daysAhead = 3. Present the first 3-5 available slots and ask the user to pick.
162-
4b. ${bold}If THEIR calendar (Option B):${bold} call check_availability_public with the event type slug, username, startDate = today, daysAhead = 3. Present the first 3-5 available slots and ask the user to pick.
163-
5. Do NOT ask "what date/time?" — the user already said they want the soonest.
173+
3. If only 1 non-hidden event type, auto-select it and immediately chain to check availability. If 2+, ask which one.
174+
4. Call check_availability (Option A) or check_availability_public (Option B) with startDate = today, daysAhead = 3.
175+
5. ${bold}If slots are returned:${bold} auto-select \`slots[0]\` (the earliest). Do NOT show a list. Present a single confirmation: "Booking [Title] with [Person] on [Date] at [Time]. Confirm?"
176+
6. ${bold}If 0 slots in 3 days:${bold} the tool response includes \`nextAvailableSlots\` (extended ${EXTENDED_SEARCH_DAYS}-day search). Use \`nextAvailableSlots[0]\` instead. Do NOT re-call the tool. Say: "No slots in the next 3 days. The earliest available is [date/time]. Book that? Or I can show more options."
177+
7. On "yes" → book. On "no" / "different time" → show the next 3-5 slots from the same response.
178+
8. Do NOT ask "what date/time?" — the user already said they want the soonest.
164179
- IMPORTANT: When the user picks an event type in a follow-up message (e.g. "15 min meeting"), check [CACHED TOOL DATA] for \`_booking_intent\`. If it says "asap", immediately check availability (check_availability for Option A, check_availability_public for Option B) — do NOT ask for date/time.
165180
166181
DURATION VALIDATION:
@@ -743,6 +758,8 @@ function createCalTools(teamId: string, userId: string, platform: string, lookup
743758
return {
744759
username,
745760
error: `No public event types found for username "${username}". The user may not exist or has no public event types.`,
761+
fallbackSuggestion:
762+
"Offer to book on the requesting user's own calendar (Option A) with the attendee's name and email instead.",
746763
};
747764
}
748765
return {
@@ -759,9 +776,15 @@ function createCalTools(teamId: string, userId: string, platform: string, lookup
759776
})),
760777
};
761778
} catch (err) {
779+
const message = err instanceof Error ? err.message : "Failed to fetch event types for this user";
780+
const isNetwork = message.includes("fetch") || message.includes("timeout") || message.includes("ECONNREFUSED");
762781
return {
763782
username,
764-
error: err instanceof Error ? err.message : "Failed to fetch event types for this user",
783+
error: message,
784+
retryable: isNetwork,
785+
fallbackSuggestion: isNetwork
786+
? "Cal.com is not responding. Ask the user to try again in a moment."
787+
: "Username may not exist. Ask if they have the correct Cal.com username, or offer to book on the user's own calendar (Option A) with name and email.",
765788
};
766789
}
767790
},
@@ -1073,7 +1096,18 @@ function createCalTools(teamId: string, userId: string, platform: string, lookup
10731096
manageUrl: `${CALCOM_APP_URL}/bookings`,
10741097
};
10751098
} catch (err) {
1076-
return { error: err instanceof Error ? err.message : "Failed to create booking" };
1099+
const message = err instanceof Error ? err.message : "Failed to create booking";
1100+
const isConflict = message.includes("409") || message.includes("conflict") || message.includes("already booked");
1101+
const isNetwork = message.includes("fetch") || message.includes("timeout") || message.includes("ECONNREFUSED");
1102+
return {
1103+
error: message,
1104+
retryable: isConflict || isNetwork,
1105+
suggestion: isConflict
1106+
? "The slot was just taken. Offer to check for other available times."
1107+
: isNetwork
1108+
? "Cal.com is not responding. Ask the user to try again in a moment."
1109+
: undefined,
1110+
};
10771111
}
10781112
},
10791113
}),
@@ -1167,7 +1201,18 @@ function createCalTools(teamId: string, userId: string, platform: string, lookup
11671201
attendees: booking.attendees.map((a) => ({ name: a.name, email: a.email })),
11681202
};
11691203
} catch (err) {
1170-
return { error: err instanceof Error ? err.message : "Failed to create booking" };
1204+
const message = err instanceof Error ? err.message : "Failed to create booking";
1205+
const isConflict = message.includes("409") || message.includes("conflict") || message.includes("already booked");
1206+
const isNetwork = message.includes("fetch") || message.includes("timeout") || message.includes("ECONNREFUSED");
1207+
return {
1208+
error: message,
1209+
retryable: isConflict || isNetwork,
1210+
suggestion: isConflict
1211+
? "The slot was just taken. Offer to check for other available times."
1212+
: isNetwork
1213+
? "Cal.com is not responding. Ask the user to try again in a moment."
1214+
: undefined,
1215+
};
11711216
}
11721217
},
11731218
}),

0 commit comments

Comments
 (0)