@agent-native/core 0.100.0 → 0.100.1
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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/templates/calendar/AGENTS.md +9 -0
- package/corpus/templates/calendar/actions/list-events.ts +659 -44
- package/corpus/templates/calendar/changelog/2026-07-13-calendar-inventory-reads-report-source-coverage.md +6 -0
- package/corpus/templates/calendar/server/lib/calendar-connector-catalog.ts +8 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +173 -51
- package/corpus/templates/calendar/server/lib/ical-fetcher.ts +13 -2
- package/corpus/templates/calendar/server/plugins/agent-chat.ts +5 -3
- package/corpus/templates/calendar/shared/api.ts +2 -0
- package/corpus/templates/mail/AGENTS.md +12 -0
- package/corpus/templates/mail/actions/list-emails.ts +486 -4
- package/corpus/templates/mail/changelog/2026-07-13-coverage-aware-connected-inbox-inventory.md +6 -0
- package/corpus/templates/mail/server/db/schema.ts +17 -0
- package/corpus/templates/mail/server/lib/google-auth.ts +33 -5
- package/corpus/templates/mail/server/lib/inventory-cursor.ts +406 -0
- package/corpus/templates/mail/server/lib/list-inbox-emails.ts +21 -2
- package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +8 -0
- package/corpus/templates/mail/server/plugins/agent-chat.ts +4 -9
- package/corpus/templates/mail/server/plugins/db.ts +20 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { and, eq, gt, lt, or, sql } from "drizzle-orm";
|
|
4
|
+
|
|
5
|
+
import { getDb } from "../db/index.js";
|
|
6
|
+
import { mailInventoryCursors } from "../db/schema.js";
|
|
7
|
+
|
|
8
|
+
const TTL_MS = 10 * 60 * 1000;
|
|
9
|
+
const CLAIM_TTL_MS = 60 * 1000;
|
|
10
|
+
const PAGE_ITEMS_BUDGET_BYTES = 12 * 1024;
|
|
11
|
+
|
|
12
|
+
export interface MailInventoryItem {
|
|
13
|
+
id: string;
|
|
14
|
+
threadId: string;
|
|
15
|
+
accountEmail: string;
|
|
16
|
+
date: string;
|
|
17
|
+
from: { name?: string; email: string };
|
|
18
|
+
subject: string;
|
|
19
|
+
isUnread: boolean;
|
|
20
|
+
isStarred?: boolean;
|
|
21
|
+
messageCount: number;
|
|
22
|
+
unreadCount: number;
|
|
23
|
+
snippet?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MailInventoryError {
|
|
27
|
+
code: string;
|
|
28
|
+
message: string;
|
|
29
|
+
retryable: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MailInventoryAccountState {
|
|
33
|
+
accountEmail: string;
|
|
34
|
+
status: "ok" | "error";
|
|
35
|
+
error?: MailInventoryError;
|
|
36
|
+
providerPageToken?: string;
|
|
37
|
+
exhausted: boolean;
|
|
38
|
+
pending: MailInventoryItem[];
|
|
39
|
+
emittedCount: number;
|
|
40
|
+
/** Unique rows discovered so coverage remains visible before emission. */
|
|
41
|
+
knownCount?: number;
|
|
42
|
+
/** Account-local thread ids already emitted by an earlier output page. */
|
|
43
|
+
emittedThreadIds?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface MailInventoryCursorState {
|
|
47
|
+
queryFingerprint: string;
|
|
48
|
+
requestedAccounts: string[] | null;
|
|
49
|
+
accounts: MailInventoryAccountState[];
|
|
50
|
+
firstPage: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface MailInventoryCursorClaim {
|
|
54
|
+
id: string;
|
|
55
|
+
claimId: string;
|
|
56
|
+
ownerEmail: string;
|
|
57
|
+
queryFingerprint: string;
|
|
58
|
+
version: number;
|
|
59
|
+
state: MailInventoryCursorState;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface MailInventoryFetchResult {
|
|
63
|
+
items: MailInventoryItem[];
|
|
64
|
+
errors: Record<string, MailInventoryError>;
|
|
65
|
+
nextPageTokens: Record<string, string>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function stableValue(value: unknown): unknown {
|
|
69
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
70
|
+
if (value && typeof value === "object") {
|
|
71
|
+
return Object.fromEntries(
|
|
72
|
+
Object.entries(value as Record<string, unknown>)
|
|
73
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
74
|
+
.map(([key, child]) => [key, stableValue(child)]),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function inventoryQueryFingerprint(input: unknown): string {
|
|
81
|
+
return createHash("sha256")
|
|
82
|
+
.update(JSON.stringify(stableValue(input)))
|
|
83
|
+
.digest("hex");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function compareInventoryItems(
|
|
87
|
+
a: MailInventoryItem,
|
|
88
|
+
b: MailInventoryItem,
|
|
89
|
+
): number {
|
|
90
|
+
const date = new Date(b.date).getTime() - new Date(a.date).getTime();
|
|
91
|
+
if (date) return date;
|
|
92
|
+
const account = a.accountEmail.localeCompare(b.accountEmail);
|
|
93
|
+
if (account) return account;
|
|
94
|
+
const thread = a.threadId.localeCompare(b.threadId);
|
|
95
|
+
if (thread) return thread;
|
|
96
|
+
return a.id.localeCompare(b.id);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function dedupeAndSort(items: MailInventoryItem[]): MailInventoryItem[] {
|
|
100
|
+
const unique = new Map<string, MailInventoryItem>();
|
|
101
|
+
for (const item of items) {
|
|
102
|
+
const key = `${item.accountEmail.toLowerCase()}:${item.threadId}`;
|
|
103
|
+
const current = unique.get(key);
|
|
104
|
+
if (!current || compareInventoryItems(item, current) < 0) {
|
|
105
|
+
unique.set(key, item);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return [...unique.values()].sort(compareInventoryItems);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function applyFetch(
|
|
112
|
+
states: MailInventoryAccountState[],
|
|
113
|
+
result: MailInventoryFetchResult,
|
|
114
|
+
): void {
|
|
115
|
+
const byAccount = new Map<string, MailInventoryItem[]>();
|
|
116
|
+
for (const item of result.items) {
|
|
117
|
+
const key = item.accountEmail.toLowerCase();
|
|
118
|
+
const accountItems = byAccount.get(key) ?? [];
|
|
119
|
+
accountItems.push(item);
|
|
120
|
+
byAccount.set(key, accountItems);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const state of states) {
|
|
124
|
+
const key = state.accountEmail.toLowerCase();
|
|
125
|
+
const error = result.errors[key];
|
|
126
|
+
if (error) {
|
|
127
|
+
state.status = "error";
|
|
128
|
+
state.error = error;
|
|
129
|
+
state.exhausted = true;
|
|
130
|
+
state.providerPageToken = undefined;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const emitted = new Set(state.emittedThreadIds ?? []);
|
|
134
|
+
const before = new Set(
|
|
135
|
+
state.pending.map((item) => item.threadId).concat([...emitted]),
|
|
136
|
+
);
|
|
137
|
+
const additions = (byAccount.get(key) ?? []).filter(
|
|
138
|
+
(item) => !before.has(item.threadId),
|
|
139
|
+
);
|
|
140
|
+
state.knownCount =
|
|
141
|
+
(state.knownCount ?? state.emittedCount) + additions.length;
|
|
142
|
+
state.pending = dedupeAndSort([...state.pending, ...additions]);
|
|
143
|
+
state.providerPageToken = result.nextPageTokens[key];
|
|
144
|
+
state.exhausted = !state.providerPageToken;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function buildMailInventoryPage(
|
|
149
|
+
state: MailInventoryCursorState,
|
|
150
|
+
limit: number,
|
|
151
|
+
fetch: (
|
|
152
|
+
accounts: Array<{ accountEmail: string; pageToken?: string }>,
|
|
153
|
+
) => Promise<MailInventoryFetchResult>,
|
|
154
|
+
): Promise<{ items: MailInventoryItem[]; hasMore: boolean }> {
|
|
155
|
+
const pageLimit = Math.max(1, Math.min(Math.floor(limit), 100));
|
|
156
|
+
const refillCounts = new Map<string, number>();
|
|
157
|
+
const refillFrontiers = async () => {
|
|
158
|
+
while (true) {
|
|
159
|
+
const needsFetch = state.accounts.filter(
|
|
160
|
+
(account) =>
|
|
161
|
+
account.status === "ok" &&
|
|
162
|
+
account.pending.length === 0 &&
|
|
163
|
+
!account.exhausted,
|
|
164
|
+
);
|
|
165
|
+
if (needsFetch.length === 0) return;
|
|
166
|
+
const fetchable = needsFetch.filter((account) => {
|
|
167
|
+
const key = account.accountEmail.toLowerCase();
|
|
168
|
+
const count = refillCounts.get(key) ?? 0;
|
|
169
|
+
if (count < 4) {
|
|
170
|
+
refillCounts.set(key, count + 1);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
account.status = "error";
|
|
174
|
+
account.error = {
|
|
175
|
+
code: "pagination_limit",
|
|
176
|
+
message:
|
|
177
|
+
"Mail pagination did not expose a row frontier within four provider pages.",
|
|
178
|
+
retryable: true,
|
|
179
|
+
};
|
|
180
|
+
account.exhausted = true;
|
|
181
|
+
account.providerPageToken = undefined;
|
|
182
|
+
return false;
|
|
183
|
+
});
|
|
184
|
+
if (fetchable.length === 0) continue;
|
|
185
|
+
const prior = fetchable.map((account) => ({
|
|
186
|
+
account,
|
|
187
|
+
token: account.providerPageToken,
|
|
188
|
+
}));
|
|
189
|
+
applyFetch(
|
|
190
|
+
fetchable,
|
|
191
|
+
await fetch(
|
|
192
|
+
fetchable.map((account) => ({
|
|
193
|
+
accountEmail: account.accountEmail,
|
|
194
|
+
pageToken: account.providerPageToken,
|
|
195
|
+
})),
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
for (const { account, token } of prior) {
|
|
199
|
+
if (
|
|
200
|
+
account.status === "ok" &&
|
|
201
|
+
!account.exhausted &&
|
|
202
|
+
account.pending.length === 0 &&
|
|
203
|
+
account.providerPageToken === token
|
|
204
|
+
) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Mail provider pagination made no progress for ${account.accountEmail}.`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const page: MailInventoryItem[] = [];
|
|
214
|
+
const emittedKeys = new Set<string>();
|
|
215
|
+
let pageBytes = 2; // JSON array brackets
|
|
216
|
+
const emit = (
|
|
217
|
+
account: MailInventoryAccountState,
|
|
218
|
+
item: MailInventoryItem,
|
|
219
|
+
) => {
|
|
220
|
+
const key = `${item.accountEmail.toLowerCase()}:${item.threadId}`;
|
|
221
|
+
if (emittedKeys.has(key)) return;
|
|
222
|
+
emittedKeys.add(key);
|
|
223
|
+
page.push(item);
|
|
224
|
+
account.emittedCount += 1;
|
|
225
|
+
account.emittedThreadIds = [
|
|
226
|
+
...(account.emittedThreadIds ?? []),
|
|
227
|
+
item.threadId,
|
|
228
|
+
];
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
while (page.length < pageLimit) {
|
|
232
|
+
// A global merge is only safe when every live account has a known
|
|
233
|
+
// frontier. Refill an emptied account before emitting an older candidate
|
|
234
|
+
// from another account; otherwise an unseen newer provider row can be
|
|
235
|
+
// skipped across the page boundary.
|
|
236
|
+
await refillFrontiers();
|
|
237
|
+
const candidates = state.accounts
|
|
238
|
+
.filter((account) => account.pending.length > 0)
|
|
239
|
+
.map((account) => ({ account, item: account.pending[0] }))
|
|
240
|
+
.sort((a, b) => compareInventoryItems(a.item, b.item));
|
|
241
|
+
if (candidates.length === 0) {
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
const winner = candidates[0];
|
|
245
|
+
const itemBytes = new TextEncoder().encode(
|
|
246
|
+
JSON.stringify(winner.item),
|
|
247
|
+
).byteLength;
|
|
248
|
+
if (
|
|
249
|
+
page.length > 0 &&
|
|
250
|
+
pageBytes + itemBytes + 1 > PAGE_ITEMS_BUDGET_BYTES
|
|
251
|
+
) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
winner.account.pending.shift();
|
|
255
|
+
emit(winner.account, winner.item);
|
|
256
|
+
pageBytes += itemBytes + (page.length > 1 ? 1 : 0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
page.sort(compareInventoryItems);
|
|
260
|
+
state.firstPage = false;
|
|
261
|
+
return {
|
|
262
|
+
items: page,
|
|
263
|
+
hasMore: state.accounts.some(
|
|
264
|
+
(account) => account.pending.length > 0 || !account.exhausted,
|
|
265
|
+
),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function createInventoryCursor(
|
|
270
|
+
ownerEmail: string,
|
|
271
|
+
state: MailInventoryCursorState,
|
|
272
|
+
): Promise<string> {
|
|
273
|
+
const now = Date.now();
|
|
274
|
+
const id = crypto.randomUUID();
|
|
275
|
+
await getDb()
|
|
276
|
+
.insert(mailInventoryCursors)
|
|
277
|
+
.values({
|
|
278
|
+
id,
|
|
279
|
+
ownerEmail,
|
|
280
|
+
queryFingerprint: state.queryFingerprint,
|
|
281
|
+
state: JSON.stringify(state),
|
|
282
|
+
version: 1,
|
|
283
|
+
expiresAt: now + TTL_MS,
|
|
284
|
+
updatedAt: now,
|
|
285
|
+
});
|
|
286
|
+
await getDb()
|
|
287
|
+
.delete(mailInventoryCursors)
|
|
288
|
+
.where(lt(mailInventoryCursors.expiresAt, now));
|
|
289
|
+
return id;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Atomically leases a cursor without consuming it. Provider work happens only
|
|
294
|
+
* after this short-lived CAS. Failures can release the lease; success settles
|
|
295
|
+
* it into a distinct successor id (or deletes it at exhaustion).
|
|
296
|
+
*/
|
|
297
|
+
export async function claimInventoryCursor(
|
|
298
|
+
ownerEmail: string,
|
|
299
|
+
id: string,
|
|
300
|
+
queryFingerprint: string,
|
|
301
|
+
): Promise<MailInventoryCursorClaim | null> {
|
|
302
|
+
const now = Date.now();
|
|
303
|
+
const claimId = crypto.randomUUID();
|
|
304
|
+
const staleBefore = now - CLAIM_TTL_MS;
|
|
305
|
+
const rows = await getDb()
|
|
306
|
+
.update(mailInventoryCursors)
|
|
307
|
+
.set({ claimId, claimedAt: now, updatedAt: now })
|
|
308
|
+
.where(
|
|
309
|
+
and(
|
|
310
|
+
eq(mailInventoryCursors.id, id),
|
|
311
|
+
eq(mailInventoryCursors.ownerEmail, ownerEmail),
|
|
312
|
+
eq(mailInventoryCursors.queryFingerprint, queryFingerprint),
|
|
313
|
+
gt(mailInventoryCursors.expiresAt, now),
|
|
314
|
+
or(
|
|
315
|
+
sql`${mailInventoryCursors.claimId} IS NULL`,
|
|
316
|
+
lt(mailInventoryCursors.claimedAt, staleBefore),
|
|
317
|
+
),
|
|
318
|
+
),
|
|
319
|
+
)
|
|
320
|
+
.returning({
|
|
321
|
+
state: mailInventoryCursors.state,
|
|
322
|
+
expiresAt: mailInventoryCursors.expiresAt,
|
|
323
|
+
version: mailInventoryCursors.version,
|
|
324
|
+
});
|
|
325
|
+
const row = rows[0];
|
|
326
|
+
if (!row || row.expiresAt <= now) return null;
|
|
327
|
+
try {
|
|
328
|
+
return {
|
|
329
|
+
id,
|
|
330
|
+
claimId,
|
|
331
|
+
ownerEmail,
|
|
332
|
+
queryFingerprint,
|
|
333
|
+
version: row.version,
|
|
334
|
+
state: JSON.parse(row.state) as MailInventoryCursorState,
|
|
335
|
+
};
|
|
336
|
+
} catch {
|
|
337
|
+
await releaseInventoryCursorClaim({
|
|
338
|
+
id,
|
|
339
|
+
claimId,
|
|
340
|
+
ownerEmail,
|
|
341
|
+
queryFingerprint,
|
|
342
|
+
version: row.version,
|
|
343
|
+
state: {} as MailInventoryCursorState,
|
|
344
|
+
});
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function releaseInventoryCursorClaim(
|
|
350
|
+
claim: MailInventoryCursorClaim,
|
|
351
|
+
): Promise<void> {
|
|
352
|
+
await getDb()
|
|
353
|
+
.update(mailInventoryCursors)
|
|
354
|
+
.set({ claimId: null, claimedAt: null, updatedAt: Date.now() })
|
|
355
|
+
.where(
|
|
356
|
+
and(
|
|
357
|
+
eq(mailInventoryCursors.id, claim.id),
|
|
358
|
+
eq(mailInventoryCursors.ownerEmail, claim.ownerEmail),
|
|
359
|
+
eq(mailInventoryCursors.queryFingerprint, claim.queryFingerprint),
|
|
360
|
+
eq(mailInventoryCursors.version, claim.version),
|
|
361
|
+
eq(mailInventoryCursors.claimId, claim.claimId),
|
|
362
|
+
),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Atomically consumes a leased id and optionally creates its successor. */
|
|
367
|
+
export async function settleInventoryCursorClaim(
|
|
368
|
+
claim: MailInventoryCursorClaim,
|
|
369
|
+
state: MailInventoryCursorState,
|
|
370
|
+
hasMore: boolean,
|
|
371
|
+
): Promise<string | undefined> {
|
|
372
|
+
const db = getDb();
|
|
373
|
+
const successorId = hasMore ? crypto.randomUUID() : undefined;
|
|
374
|
+
const now = Date.now();
|
|
375
|
+
return db.transaction(async (tx: any) => {
|
|
376
|
+
const consumed = await tx
|
|
377
|
+
.delete(mailInventoryCursors)
|
|
378
|
+
.where(
|
|
379
|
+
and(
|
|
380
|
+
eq(mailInventoryCursors.id, claim.id),
|
|
381
|
+
eq(mailInventoryCursors.ownerEmail, claim.ownerEmail),
|
|
382
|
+
eq(mailInventoryCursors.queryFingerprint, claim.queryFingerprint),
|
|
383
|
+
eq(mailInventoryCursors.version, claim.version),
|
|
384
|
+
eq(mailInventoryCursors.claimId, claim.claimId),
|
|
385
|
+
),
|
|
386
|
+
)
|
|
387
|
+
.returning({ id: mailInventoryCursors.id });
|
|
388
|
+
if (consumed.length !== 1) {
|
|
389
|
+
throw new Error("Inventory cursor lease was lost before settlement.");
|
|
390
|
+
}
|
|
391
|
+
if (successorId) {
|
|
392
|
+
await tx.insert(mailInventoryCursors).values({
|
|
393
|
+
id: successorId,
|
|
394
|
+
ownerEmail: claim.ownerEmail,
|
|
395
|
+
queryFingerprint: claim.queryFingerprint,
|
|
396
|
+
state: JSON.stringify(state),
|
|
397
|
+
version: claim.version + 1,
|
|
398
|
+
claimId: null,
|
|
399
|
+
claimedAt: null,
|
|
400
|
+
expiresAt: now + TTL_MS,
|
|
401
|
+
updatedAt: now,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return successorId;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
@@ -45,7 +45,11 @@ export interface ListInboxEmailsParams {
|
|
|
45
45
|
pageTokens?: Record<string, string>;
|
|
46
46
|
threadFormat?: "full" | "metadata" | "minimal";
|
|
47
47
|
threadCandidateLimit?: number;
|
|
48
|
+
/** Disable the extra recent-message candidate pass for bounded inventory. */
|
|
49
|
+
includeRecentMessageCandidates?: boolean;
|
|
48
50
|
accountTokens: ListInboxEmailsAccountToken[];
|
|
51
|
+
/** Selected account ids are forwarded to Gmail before token refresh. */
|
|
52
|
+
accountEmails?: string[];
|
|
49
53
|
labelMap: Map<string, string>;
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -108,7 +112,9 @@ export async function listInboxEmails(
|
|
|
108
112
|
pageTokens,
|
|
109
113
|
threadFormat,
|
|
110
114
|
threadCandidateLimit,
|
|
115
|
+
includeRecentMessageCandidates = true,
|
|
111
116
|
accountTokens,
|
|
117
|
+
accountEmails,
|
|
112
118
|
labelMap,
|
|
113
119
|
} = params;
|
|
114
120
|
|
|
@@ -123,12 +129,25 @@ export async function listInboxEmails(
|
|
|
123
129
|
threadFormat,
|
|
124
130
|
threadCandidateLimit,
|
|
125
131
|
threadRecentMessageCandidateLimit:
|
|
126
|
-
|
|
132
|
+
includeRecentMessageCandidates &&
|
|
133
|
+
!q &&
|
|
134
|
+
(view === "inbox" || view === "unread")
|
|
127
135
|
? DEFAULT_THREAD_RECENT_MESSAGE_CANDIDATE_LIMIT
|
|
128
136
|
: undefined,
|
|
137
|
+
accountEmails,
|
|
129
138
|
});
|
|
130
139
|
|
|
131
|
-
|
|
140
|
+
const failedAccounts = new Set(
|
|
141
|
+
errors.map((error) => error.email.toLowerCase()),
|
|
142
|
+
);
|
|
143
|
+
const everySelectedAccountFailed = [...connectedEmails].every((email) =>
|
|
144
|
+
failedAccounts.has(email),
|
|
145
|
+
);
|
|
146
|
+
if (
|
|
147
|
+
messages.length === 0 &&
|
|
148
|
+
errors.length > 0 &&
|
|
149
|
+
everySelectedAccountFailed
|
|
150
|
+
) {
|
|
132
151
|
const isQuotaError = errors.every((e) => isGmailQuotaError(e.error));
|
|
133
152
|
return {
|
|
134
153
|
ok: false,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deliberately narrow authenticated MCP surface for Mail.
|
|
3
|
+
*
|
|
4
|
+
* External callers may read inbox coverage through list-emails. Other actions
|
|
5
|
+
* remain available through the in-app agent, ask_app, or an explicit
|
|
6
|
+
* full-catalog connection; tool-search alone never makes them callable.
|
|
7
|
+
*/
|
|
8
|
+
export const MAIL_CONNECTOR_CATALOG = ["list-emails"] as const;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "@agent-native/core/server";
|
|
7
7
|
|
|
8
8
|
import actionsRegistry from "../../.generated/actions-registry.js";
|
|
9
|
+
import { MAIL_CONNECTOR_CATALOG } from "../lib/mail-connector-catalog.js";
|
|
9
10
|
|
|
10
11
|
const INITIAL_TOOL_NAMES = [
|
|
11
12
|
"view-screen",
|
|
@@ -33,6 +34,7 @@ export default createAgentChatPlugin({
|
|
|
33
34
|
actions: loadActionsFromStaticRegistry(actionsRegistry),
|
|
34
35
|
appId: "mail",
|
|
35
36
|
initialToolNames: INITIAL_TOOL_NAMES,
|
|
37
|
+
connectorCatalog: [...MAIL_CONNECTOR_CATALOG],
|
|
36
38
|
resolveOrgId: async (event) => {
|
|
37
39
|
const ctx = await getOrgContext(event);
|
|
38
40
|
return ctx.orgId;
|
|
@@ -90,16 +92,9 @@ export default createAgentChatPlugin({
|
|
|
90
92
|
|
|
91
93
|
Some less-common tool schemas are loaded on demand. Use tool-search with a specific query when you need a capability that is not already available as a direct tool.
|
|
92
94
|
|
|
93
|
-
##
|
|
95
|
+
## Deterministic Mail Reads
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
If view-screen shows 0 emails or indicates Google is not connected:
|
|
97
|
-
- Do NOT run list-emails, search-emails, send-email, or any email operation scripts
|
|
98
|
-
- Do NOT pretend to have access to emails
|
|
99
|
-
- Tell the user: "You need to connect your Google account first. Click the 'Connect Google' button on the main screen to get started."
|
|
100
|
-
- You can still answer general questions, but you cannot perform any email operations
|
|
101
|
-
|
|
102
|
-
Only proceed with email operations if view-screen confirms real emails are available.
|
|
97
|
+
For deterministic headless email reads, call list-emails directly in inventory/coverage mode. Do not require view-screen as a Google connection preflight: list-emails selects the connected Gmail or synthetic local-mail backend for the user and returns the relevant result. Use view-screen only when the answer depends on visible UI state, such as the active thread, selected message, draft, queue item, or current inbox view. Treat real action errors as the evidence for an unavailable connection; do not infer it from a zero-email screen.
|
|
103
98
|
|
|
104
99
|
Available operations:
|
|
105
100
|
- List and search emails
|
|
@@ -188,6 +188,26 @@ CREATE INDEX IF NOT EXISTS idx_snippets_owner_name ON snippets(owner_email, name
|
|
|
188
188
|
sql: `ALTER TABLE queued_email_drafts ADD COLUMN IF NOT EXISTS send_claim_id TEXT;
|
|
189
189
|
ALTER TABLE queued_email_drafts ADD COLUMN IF NOT EXISTS send_claimed_at ${intType()}`,
|
|
190
190
|
},
|
|
191
|
+
{
|
|
192
|
+
version: 18,
|
|
193
|
+
name: "mail-inventory-cursors",
|
|
194
|
+
sql: `CREATE TABLE IF NOT EXISTS mail_inventory_cursors (
|
|
195
|
+
id TEXT PRIMARY KEY,
|
|
196
|
+
owner_email TEXT NOT NULL,
|
|
197
|
+
query_fingerprint TEXT NOT NULL,
|
|
198
|
+
state TEXT NOT NULL,
|
|
199
|
+
version ${intType()} NOT NULL DEFAULT 1,
|
|
200
|
+
expires_at ${intType()} NOT NULL,
|
|
201
|
+
updated_at ${intType()} NOT NULL
|
|
202
|
+
);
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_mail_inventory_cursors_owner_expiry ON mail_inventory_cursors(owner_email, expires_at);`,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
version: 19,
|
|
207
|
+
name: "mail-inventory-cursor-leases",
|
|
208
|
+
sql: `ALTER TABLE mail_inventory_cursors ADD COLUMN IF NOT EXISTS claim_id TEXT;
|
|
209
|
+
ALTER TABLE mail_inventory_cursors ADD COLUMN IF NOT EXISTS claimed_at ${intType()}`,
|
|
210
|
+
},
|
|
191
211
|
],
|
|
192
212
|
{ table: "mail_migrations" },
|
|
193
213
|
);
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
ok?: undefined;
|
|
17
16
|
error: string;
|
|
17
|
+
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
|
|
14
14
|
count: number;
|
|
15
15
|
updated?: undefined;
|
|
16
|
-
ok?: undefined;
|
|
17
16
|
error?: undefined;
|
|
17
|
+
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
19
|
count?: undefined;
|
|
20
20
|
updated: number;
|
|
21
|
-
ok?: undefined;
|
|
22
21
|
error?: undefined;
|
|
22
|
+
ok?: undefined;
|
|
23
23
|
} | {
|
|
24
24
|
count?: undefined;
|
|
25
25
|
updated?: undefined;
|
|
@@ -28,7 +28,7 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
|
|
|
28
28
|
} | {
|
|
29
29
|
count?: undefined;
|
|
30
30
|
updated?: undefined;
|
|
31
|
-
ok: boolean;
|
|
32
31
|
error?: undefined;
|
|
32
|
+
ok: boolean;
|
|
33
33
|
}>>;
|
|
34
34
|
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -52,8 +52,8 @@ export declare function handleDeleteResource(event: any): Promise<{
|
|
|
52
52
|
error: string;
|
|
53
53
|
ok?: undefined;
|
|
54
54
|
} | {
|
|
55
|
-
error?: undefined;
|
|
56
55
|
ok: boolean;
|
|
56
|
+
error?: undefined;
|
|
57
57
|
}>;
|
|
58
58
|
/** POST /_agent-native/resources/upload — upload a file as a resource */
|
|
59
59
|
export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
|
|
@@ -74,10 +74,10 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
|
|
|
74
74
|
runId: string | null;
|
|
75
75
|
expiresAt: number | null;
|
|
76
76
|
metadata: string | null;
|
|
77
|
-
error?: undefined;
|
|
78
77
|
url: string;
|
|
79
78
|
provider: string;
|
|
80
79
|
storageSetupRequired?: undefined;
|
|
80
|
+
error?: undefined;
|
|
81
81
|
} | {
|
|
82
82
|
error: string;
|
|
83
83
|
storageSetupRequired: boolean;
|
|
@@ -27,11 +27,11 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
27
27
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
28
28
|
error: any;
|
|
29
29
|
} | {
|
|
30
|
+
error?: undefined;
|
|
30
31
|
ok: boolean;
|
|
31
32
|
key: string;
|
|
32
33
|
baseUrlKey?: string;
|
|
33
34
|
scope: AgentEngineApiKeyScope;
|
|
34
|
-
error?: undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
export {};
|
|
37
37
|
//# sourceMappingURL=agent-engine-api-key-route.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.100.
|
|
3
|
+
"version": "0.100.1",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|