Skip to content

Commit 7977f44

Browse files
authored
Merge branch 'main' into tmi/issue-1088-static-mount-per-request
2 parents 20baf94 + 292f708 commit 7977f44

3 files changed

Lines changed: 481 additions & 22 deletions

File tree

hub/agents/python/email/gaia_agent_email/agent.py

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class never passes ``use_claude=True`` / ``use_chatgpt=True`` to
6666
from gaia.agents.base.memory import MemoryMixin
6767
from gaia.agents.base.tools import _TOOL_REGISTRY
6868
from gaia.connectors.errors import ConnectorsError
69+
from gaia.connectors.formatting import format_connector_error
6970
from gaia.connectors.providers.base import ConnectorRequirement
7071
from gaia.database.mixin import DatabaseMixin
7172
from gaia.llm.lemonade_client import DEFAULT_MODEL_NAME
@@ -489,6 +490,13 @@ def _triage_all_backends(self, *, max_messages: int) -> dict:
489490
because local inference is slow (~9-31 s/email) and a doubled budget
490491
would blow the user's expected wait. Every returned item gains a
491492
``mailbox`` tag and its id is remembered for downstream action routing.
493+
494+
When one backend raises ``ConnectorsError`` (e.g. an agent grant was
495+
revoked while the connection remains live), the error is recorded as a
496+
per-mailbox notice in ``mailbox_errors`` and the loop continues with the
497+
remaining backends. Non-``ConnectorsError`` exceptions still propagate —
498+
a genuine bug must fail loudly. The available set stays connection-derived;
499+
grant enforcement happens at the token layer.
492500
"""
493501
from gaia_agent_email.tools import read_tools
494502
from gaia_agent_email.tools.read_tools import (
@@ -509,17 +517,24 @@ def _triage_all_backends(self, *, max_messages: int) -> dict:
509517
backends = self._backends
510518
per_backend = max(1, max_messages // len(backends))
511519
merged: list[dict] = []
520+
mailbox_errors: list[dict] = []
512521
for provider, backend in backends.items():
513522
if len(merged) >= max_messages:
514523
break
515-
out = triage_inbox_impl(
516-
backend,
517-
max_messages=per_backend,
518-
session_preferences=prefs,
519-
force_llm=force_llm,
520-
classifier=classifier,
521-
debug=debug_flag,
522-
)
524+
try:
525+
out = triage_inbox_impl(
526+
backend,
527+
max_messages=per_backend,
528+
session_preferences=prefs,
529+
force_llm=force_llm,
530+
classifier=classifier,
531+
debug=debug_flag,
532+
)
533+
except ConnectorsError as exc:
534+
msg = format_connector_error(exc)
535+
mailbox_errors.append({"mailbox": provider, "error": msg})
536+
logger.warning("email triage: skipping %s mailbox — %s", provider, msg)
537+
continue
523538
for item in out["results"]:
524539
item["mailbox"] = provider
525540
self._remember_message_mailbox(item.get("id"), provider)
@@ -541,7 +556,17 @@ def _triage_all_backends(self, *, max_messages: int) -> dict:
541556
self._apply_behavioral_promotions()
542557
# Re-group the merged, capped list so the bucketed view matches what the
543558
# caller actually sees.
544-
return {"results": merged, "grouped": group_by_category(merged)}
559+
if mailbox_errors and len(mailbox_errors) == len(self._backends):
560+
# Every connected mailbox failed — surface it loudly rather than
561+
# returning ok with zero results (which reads as "empty inbox").
562+
raise ConnectorsError(
563+
"All connected mailboxes failed during triage: "
564+
+ "; ".join(f"{e['mailbox']}: {e['error']}" for e in mailbox_errors)
565+
)
566+
result: dict = {"results": merged, "grouped": group_by_category(merged)}
567+
if mailbox_errors:
568+
result["mailbox_errors"] = mailbox_errors
569+
return result
545570

546571
def _apply_behavioral_promotions(self) -> None:
547572
"""Promote qualifying senders to priority based on observed reply behavior.
@@ -593,6 +618,10 @@ def _pre_scan_all_backends(self, *, max_messages: int) -> dict:
593618
(urgent / actionable / suggested_archives) gains a ``mailbox`` tag and
594619
its message_id is remembered for action routing. Per-section caps and
595620
the envelope shape are preserved by merging the per-backend envelopes.
621+
622+
When one backend raises ``ConnectorsError`` (e.g. a revoked agent grant),
623+
the error is recorded in ``mailbox_errors`` and the loop continues with
624+
the remaining backends. Non-``ConnectorsError`` exceptions still propagate.
596625
"""
597626
from gaia_agent_email.tools.read_tools import (
598627
PRE_SCAN_ACTIONABLE_CAP,
@@ -613,16 +642,25 @@ def _pre_scan_all_backends(self, *, max_messages: int) -> dict:
613642
informational_count = 0
614643
scanned = 0
615644
merged_prefs_applied: dict = {}
645+
mailbox_errors: list[dict] = []
616646
for provider, backend in backends.items():
617647
if scanned >= max_messages:
618648
break
619-
out = pre_scan_inbox_impl(
620-
backend,
621-
max_messages=per_backend,
622-
session_preferences=prefs,
623-
force_llm=force_llm,
624-
debug=debug_flag,
625-
)
649+
try:
650+
out = pre_scan_inbox_impl(
651+
backend,
652+
max_messages=per_backend,
653+
session_preferences=prefs,
654+
force_llm=force_llm,
655+
debug=debug_flag,
656+
)
657+
except ConnectorsError as exc:
658+
msg = format_connector_error(exc)
659+
mailbox_errors.append({"mailbox": provider, "error": msg})
660+
logger.warning(
661+
"email pre-scan: skipping %s mailbox — %s", provider, msg
662+
)
663+
continue
626664
# Count messages actually returned, not the cap — an under-filled
627665
# backend would otherwise trip the budget guard and skip a later one.
628666
backend_totals = out.get("totals", {})
@@ -649,7 +687,7 @@ def _pre_scan_all_backends(self, *, max_messages: int) -> dict:
649687
self._remember_message_mailbox(item.get("thread_id"), provider)
650688
suggested_archives.append(item)
651689
informational_count += int(out.get("informational_count", 0))
652-
return {
690+
result = {
653691
"kind": "email_pre_scan",
654692
"urgent": urgent[: max(0, PRE_SCAN_URGENT_CAP)],
655693
"actionable": actionable[: max(0, PRE_SCAN_ACTIONABLE_CAP)],
@@ -664,6 +702,16 @@ def _pre_scan_all_backends(self, *, max_messages: int) -> dict:
664702
"suggested_archives": len(suggested_archives),
665703
},
666704
}
705+
if mailbox_errors and len(mailbox_errors) == len(self._backends):
706+
# Every connected mailbox failed — surface it loudly rather than
707+
# returning ok with zero results (which reads as "empty inbox").
708+
raise ConnectorsError(
709+
"All connected mailboxes failed during pre-scan: "
710+
+ "; ".join(f"{e['mailbox']}: {e['error']}" for e in mailbox_errors)
711+
)
712+
if mailbox_errors:
713+
result["mailbox_errors"] = mailbox_errors
714+
return result
667715

668716
# -- Phase I3 batch-organize counter -----------------------------------
669717

hub/agents/python/email/gaia_agent_email/config.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,11 +249,17 @@ def resolve_mail_backends(self) -> List[Tuple[str, Any]]:
249249
- ``"google"`` / ``"microsoft"`` → only that provider, and only when
250250
it is actually connected.
251251
252-
Connector-derived: the connected set comes from
253-
``connected_mailbox_providers()`` (the keyring), in registry order
254-
(google before microsoft). Fails loudly — an explicit filter naming an
255-
unconnected provider, or nothing connected at all, raises
256-
``ConfigurationError`` rather than silently triaging one mailbox.
252+
Connector-derived (intentional): the available set is the set of
253+
CONNECTED providers, not the set of providers the agent is granted for.
254+
Grant enforcement is the connectors layer's job — ``get_access_token_sync``
255+
raises ``AuthRequiredError(AGENT_NOT_GRANTED)`` eagerly when the token is
256+
fetched. The agent catches ``ConnectorsError`` per mailbox in
257+
``_triage_all_backends`` / ``_pre_scan_all_backends`` and surfaces a clean,
258+
actionable per-mailbox notice rather than aborting the whole scan.
259+
260+
Fails loudly — an explicit filter naming an unconnected provider, or
261+
nothing connected at all, raises ``ConfigurationError`` rather than
262+
silently triaging one mailbox.
257263
258264
The per-provider eval seam (``gmail_backend`` / ``outlook_backend``) is
259265
honored via ``_build_mail_backend``. An injected backend also marks its
@@ -322,6 +328,12 @@ def resolve_calendar_backend(self) -> Any:
322328
interchangeably. An unsupported explicit provider raises
323329
``ConfigurationError`` (fail loudly).
324330
331+
Grant enforcement is the connectors layer's job — a calendar backend
332+
whose agent grant has been revoked raises ``AuthRequiredError`` when the
333+
first calendar tool call fetches the token. The existing per-tool
334+
``ConnectorsError`` handler in ``CalendarToolsMixin`` surfaces that as a
335+
clean actionable envelope without requiring any grant reasoning here.
336+
325337
Live backend imports are local to keep the module import graph free of
326338
the ``connectors`` dependency chain at ``config`` import time.
327339
"""

0 commit comments

Comments
 (0)