diff --git a/packages/junior-google/src/tools/find-meeting-times.ts b/packages/junior-google/src/tools/find-meeting-times.ts index c17564a537..0de934061f 100644 --- a/packages/junior-google/src/tools/find-meeting-times.ts +++ b/packages/junior-google/src/tools/find-meeting-times.ts @@ -28,12 +28,12 @@ const inputSchema = z attendees: emailListSchema(20) .min(1) .describe( - "Work emails of the people who must attend. Resolve names with userLookup. Junior adds the requester automatically when it knows their email.", + "Work emails of the people who must attend. Resolve names with userLookup. The runtime adds the requester as required when it knows their email. To make the requester optional, put their email in optionalAttendees.", ), optionalAttendees: emailListSchema(20) .default([]) .describe( - 'Work emails of people the requester calls optional or "if free". Their conflicts do not block a slot; slots where they are free come first.', + 'Work emails of people the requester calls optional or "if free". Their conflicts do not block a slot; slots where they are free come first. Put the requester here when their own calendar must not block a slot, such as when they say some events are only holds.', ), durationMinutes: z.number().int().min(15).max(480).default(30), timeMin: z.iso @@ -378,9 +378,14 @@ export function createFindMeetingTimesTool(ctx: GoogleToolContext) { "workdayEnd must be after workdayStart.", ); } - const attendees = await withRequester(ctx, input.attendees); + // An explicit optional entry for the requester wins over the automatic add. + const optionalEmails = new Set(input.optionalAttendees); + const attendees = (await withRequester(ctx, input.attendees)).filter( + (email) => + input.attendees.includes(email) || !optionalEmails.has(email), + ); const required = new Set(attendees); - const optionalAttendees = [...new Set(input.optionalAttendees)].filter( + const optionalAttendees = [...optionalEmails].filter( (email) => !required.has(email), ); const everyone = [...attendees, ...optionalAttendees]; diff --git a/packages/junior-google/tests/calendar.test.ts b/packages/junior-google/tests/calendar.test.ts index 29354c63c7..45e2e4d573 100644 --- a/packages/junior-google/tests/calendar.test.ts +++ b/packages/junior-google/tests/calendar.test.ts @@ -129,7 +129,12 @@ describe("Google Calendar tools", () => { const { fetch, tools } = calendarTools( Response.json({ calendars: { - [REQUESTER]: { busy: [] }, + // The requester says this all-day block is only a hold. + [REQUESTER]: { + busy: [ + { start: "2026-10-12T16:00:00Z", end: "2026-10-13T00:00:00Z" }, + ], + }, "bob@example.com": { busy: [ { start: "2026-10-12T16:00:00Z", end: "2026-10-12T17:00:00Z" }, @@ -138,15 +143,16 @@ describe("Google Calendar tools", () => { "carol@example.com": { busy: [], errors: [{ reason: "notFound" }] }, }, }), - Response.json({ timeZone: LA }), Response.json({ timeZone: "America/New_York" }), Response.json({}, { status: 404 }), + Response.json({ timeZone: LA }), ); const result = await tools.findMeetingTimes!.execute!( tools.findMeetingTimes!.prepareArguments!({ attendees: ["Bob@example.com", "carol@example.com"], maxResults: 1, + optionalAttendees: [REQUESTER], timeMax: "2026-10-13T00:00:00Z", timeMin: "2026-10-12T16:00:00Z", timeZone: LA, @@ -158,12 +164,12 @@ describe("Google Calendar tools", () => { expect(freeBusy.operation).toBe("google.calendar.freebusy.query"); expect(freeBusy.provider).toBe("google"); expect((await freeBusy.request.json()).items).toEqual([ - { id: REQUESTER }, { id: "bob@example.com" }, { id: "carol@example.com" }, + { id: REQUESTER }, ]); - const bobZone = new URL(timeZones[1].request.url); - expect(timeZones[1].operation).toBe("google.calendar.events.list"); + const bobZone = new URL(timeZones[0].request.url); + expect(timeZones[0].operation).toBe("google.calendar.events.list"); expect(bobZone.pathname).toBe( "/calendar/v3/calendars/bob%40example.com/events", ); @@ -171,12 +177,14 @@ describe("Google Calendar tools", () => { expect(bobZone.searchParams.has("timeZone")).toBe(false); expect(result).toMatchObject({ checked: [ - { email: REQUESTER, timeZone: LA }, { email: "bob@example.com", timeZone: "America/New_York" }, + { email: REQUESTER, timeZone: LA }, ], - // 10:00 in Los Angeles, after Bob's 12:00-13:00 in New York. + // 10:00 in Los Angeles, after Bob's 12:00-13:00 in New York. The + // requester listed themselves as optional, so the hold does not block it. slots: [ { + optionalConflicts: [REQUESTER], otherTimeZoneLabels: ["Mon, Oct 12, 13:00 – 13:30 EDT"], start: "2026-10-12T17:00:00.000Z", },