@agent-native/scheduling 0.2.52 → 0.2.53-nightly-20261105110131

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/server/availability-engine.d.ts +24 -0
  2. package/dist/server/availability-engine.d.ts.map +1 -1
  3. package/dist/server/availability-engine.js +73 -5
  4. package/dist/server/availability-engine.js.map +1 -1
  5. package/dist/server/providers/errors.d.ts +22 -0
  6. package/dist/server/providers/errors.d.ts.map +1 -0
  7. package/dist/server/providers/errors.js +53 -0
  8. package/dist/server/providers/errors.js.map +1 -0
  9. package/dist/server/providers/google-calendar.d.ts +0 -11
  10. package/dist/server/providers/google-calendar.d.ts.map +1 -1
  11. package/dist/server/providers/google-calendar.js +57 -3
  12. package/dist/server/providers/google-calendar.js.map +1 -1
  13. package/dist/server/providers/index.d.ts +1 -0
  14. package/dist/server/providers/index.d.ts.map +1 -1
  15. package/dist/server/providers/index.js +1 -0
  16. package/dist/server/providers/index.js.map +1 -1
  17. package/dist/server/providers/office365.d.ts.map +1 -1
  18. package/dist/server/providers/office365.js +87 -25
  19. package/dist/server/providers/office365.js.map +1 -1
  20. package/docs/llms-full.txt +29 -8
  21. package/docs/providers.md +10 -0
  22. package/docs/skills/integrations/SKILL.md +19 -8
  23. package/package.json +4 -4
  24. package/src/server/availability-engine.test.ts +459 -23
  25. package/src/server/availability-engine.ts +110 -10
  26. package/src/server/providers/calendar-busy.test.ts +417 -0
  27. package/src/server/providers/errors.ts +66 -0
  28. package/src/server/providers/google-calendar.ts +71 -4
  29. package/src/server/providers/index.ts +1 -0
  30. package/src/server/providers/office365.ts +111 -33
@@ -1,4 +1,9 @@
1
1
  import type { BusyInterval } from "../../shared/index.js";
2
+ import {
3
+ CalendarCredentialInvalidError,
4
+ CalendarLookupRateLimitedError,
5
+ parseRetryAfterSeconds,
6
+ } from "./errors.js";
2
7
  import type { CalendarProvider } from "./types.js";
3
8
 
4
9
  export interface Office365ProviderConfig {
@@ -14,6 +19,22 @@ export interface Office365ProviderConfig {
14
19
  }
15
20
 
16
21
  const SCOPES = ["offline_access", "Calendars.ReadWrite", "User.Read"];
22
+ const MAX_CALENDAR_VIEW_CONCURRENCY = 4;
23
+ const GRAPH_TOKEN_ERROR_CODES = new Set([
24
+ "InvalidAuthenticationToken",
25
+ "AuthenticationError",
26
+ ]);
27
+
28
+ function graphErrorCode(body: string): string {
29
+ try {
30
+ const code = (JSON.parse(body) as { error?: { code?: unknown } }).error
31
+ ?.code;
32
+ return typeof code === "string" ? code : "";
33
+ // coercion-ok: a non-JSON error body has no Graph error code, so it is not a token error
34
+ } catch {
35
+ return "";
36
+ }
37
+ }
17
38
 
18
39
  export function createOffice365Provider(
19
40
  config: Office365ProviderConfig,
@@ -28,16 +49,38 @@ export function createOffice365Provider(
28
49
  const headers = new Headers(init?.headers);
29
50
  headers.set("authorization", `Bearer ${token}`);
30
51
  headers.set("content-type", "application/json");
31
- const res = await fetch(`https://graph.microsoft.com/v1.0${path}`, {
52
+ // Paging links are absolute; never send the token anywhere but Graph.
53
+ const url = path.startsWith("/")
54
+ ? `https://graph.microsoft.com/v1.0${path}`
55
+ : path;
56
+ if (!url.startsWith("https://graph.microsoft.com/")) {
57
+ throw new Error(`Refusing to call a non-Graph URL: ${url}`);
58
+ }
59
+ const res = await fetch(url, {
32
60
  ...init,
33
61
  headers,
34
62
  });
35
- if (res.status === 401 || res.status === 403) {
36
- await config.markInvalid?.(credentialId);
37
- throw new Error(`Office 365 credential invalid (${res.status})`);
63
+ if (res.status === 429) {
64
+ throw new CalendarLookupRateLimitedError(
65
+ "Graph: rate limited (429)",
66
+ parseRetryAfterSeconds(res.headers.get("retry-after")),
67
+ );
38
68
  }
39
69
  if (!res.ok) {
40
- throw new Error(`Graph ${res.status}: ${await res.text()}`);
70
+ const body = await res.text();
71
+ // A 403 is usually about one resource (a shared calendar this account
72
+ // can't read), not the token; only token errors invalidate the credential.
73
+ if (
74
+ res.status === 401 ||
75
+ (res.status === 403 &&
76
+ GRAPH_TOKEN_ERROR_CODES.has(graphErrorCode(body)))
77
+ ) {
78
+ await config.markInvalid?.(credentialId);
79
+ throw new CalendarCredentialInvalidError(
80
+ `Office 365 credential invalid (${res.status})`,
81
+ );
82
+ }
83
+ throw new Error(`Graph ${res.status}: ${body}`);
41
84
  }
42
85
  if (res.status === 204) return undefined as unknown as T;
43
86
  return (await res.json()) as T;
@@ -113,37 +156,72 @@ export function createOffice365Provider(
113
156
  primary: c.isDefaultCalendar,
114
157
  }));
115
158
  },
159
+ // Selected calendars are stored by Graph calendar id, which getSchedule
160
+ // does not accept (it takes SMTP addresses), so read each calendar's
161
+ // calendarView. Times come back in UTC without an offset.
116
162
  async getBusy({ credentialId, calendarExternalIds, start, end }) {
117
- const resp = await graph<{
118
- value: {
119
- scheduleId: string;
120
- scheduleItems: {
121
- start: { dateTime: string };
122
- end: { dateTime: string };
123
- status: string;
124
- }[];
125
- }[];
126
- }>(credentialId, "/me/calendar/getSchedule", {
127
- method: "POST",
128
- body: JSON.stringify({
129
- schedules: calendarExternalIds,
130
- startTime: { dateTime: start.toISOString(), timeZone: "UTC" },
131
- endTime: { dateTime: end.toISOString(), timeZone: "UTC" },
132
- availabilityViewInterval: 30,
133
- }),
134
- });
135
- const out: BusyInterval[] = [];
136
- for (const entry of resp.value) {
137
- for (const item of entry.scheduleItems) {
138
- if (item.status === "free") continue;
139
- out.push({
140
- start: item.start.dateTime + "Z",
141
- end: item.end.dateTime + "Z",
142
- source: entry.scheduleId,
143
- });
163
+ // One unreadable calendar fails the whole lookup, so stop the rest.
164
+ const stop = new AbortController();
165
+ async function readCalendar(calendarId: string): Promise<BusyInterval[]> {
166
+ const out: BusyInterval[] = [];
167
+ const params = new URLSearchParams({
168
+ startDateTime: start.toISOString(),
169
+ endDateTime: end.toISOString(),
170
+ $select: "start,end,showAs,isCancelled",
171
+ $top: "250",
172
+ });
173
+ let path: string | undefined =
174
+ `/me/calendars/${encodeURIComponent(calendarId)}/calendarView?${params}`;
175
+ while (path) {
176
+ const page: {
177
+ value?: {
178
+ start: { dateTime: string };
179
+ end: { dateTime: string };
180
+ showAs?: string;
181
+ isCancelled?: boolean;
182
+ }[];
183
+ "@odata.nextLink"?: string;
184
+ } = await graph(credentialId, path, { signal: stop.signal });
185
+ if (!Array.isArray(page.value)) {
186
+ throw new Error(
187
+ `Graph calendarView returned no events list for ${calendarId}`,
188
+ );
189
+ }
190
+ for (const event of page.value) {
191
+ if (event.isCancelled || event.showAs === "free") continue;
192
+ out.push({
193
+ start: event.start.dateTime + "Z",
194
+ end: event.end.dateTime + "Z",
195
+ source: calendarId,
196
+ });
197
+ }
198
+ path = page["@odata.nextLink"];
144
199
  }
200
+ return out;
145
201
  }
146
- return out;
202
+
203
+ const perCalendar: BusyInterval[][] = [];
204
+ const queue = calendarExternalIds.map((calendarId, index) => ({
205
+ calendarId,
206
+ index,
207
+ }));
208
+ async function worker(): Promise<void> {
209
+ for (let item = queue.shift(); item; item = queue.shift()) {
210
+ try {
211
+ perCalendar[item.index] = await readCalendar(item.calendarId);
212
+ } catch (error) {
213
+ queue.length = 0;
214
+ stop.abort();
215
+ throw error;
216
+ }
217
+ }
218
+ }
219
+ const workerCount = Math.min(
220
+ MAX_CALENDAR_VIEW_CONCURRENCY,
221
+ calendarExternalIds.length,
222
+ );
223
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
224
+ return perCalendar.flat();
147
225
  },
148
226
  async createEvent({ credentialId, calendarExternalId, booking }) {
149
227
  const body = {