0015 — Aggregate the copilot "Pendientes" feed through the tool registry
- Status: accepted
- Date: 2026-06-15
- Deciders: Core team
- Tags: copilot, agents, modules, rbac
Context
The IA redesign (Fase 2, docs/technical/copilot/ia-redesign-plan.md) adds a read-only Pendientes feed to the copilot drawer: open work the caller can act on — overdue recalls, budgets awaiting a response, etc. The data lives in other modules (recalls, budget, …). Copilot keeps manifest.depends = [] and must not import another module's service (ADR 0001/0003). The question the roadmap flagged: how does copilot aggregate cross-module signals for this feed?
Three mechanisms were on the table: call the modules' agent tools through the registry; import their read services directly; or react to events and maintain a copilot-owned projection table.
Decision
The Pendientes feed aggregates by calling the existing agent tools through the global tool_registry, with an AgentContext whose permissions are get_role_permissions(caller_role) — the exact same path the chat turn and the morning digest already use. No new tables, no cross-module imports.
Clarifications:
- One reusable per-clinic copilot agent + a single reusable session (metadata
surface=copilot_pending) back the audit context, so a drawer open does not insert agent/session rows each time. - RBAC parity is mechanical: the registry enforces each tool's
permissionsagainst the caller's role, so the feed can never surface anything the caller couldn't see in the owning module. - A tool whose module is uninstalled is simply absent from
tool_registry.list()and its section is omitted.
Consequences
Good
depends = []holds — copilot stays cleanly removable.- RBAC parity for free; no second authorization path to keep in sync.
- Consistent with chat + digest — one aggregation pattern in the module.
- Read tool calls land in
agent_audit_logs, so they show up in theGET /metricsdashboard like any other usage.
Bad / accepted trade-offs
- The feed is limited to what tools expose; a new pending source needs a tool (which is the right place for it anyway).
- Each pending fetch executes N tool calls inline (currently 2). Bounded and fast; if it grows, batch or cache behind the reusable session.
- Sources without a tool yet (cash mismatch, the "Hecho"/done timeline filter) are deferred until the relevant tool exists.
Alternatives considered
- Import module read services directly — rejected: breaks the
depends = []contract and duplicates the RBAC checks the tools already encapsulate. - Event-driven projection table — rejected for the live feed: eventual-consistency and a copilot-owned copy of other modules' state add bug surface for a read that the registry serves synchronously. (Events remain the right tool for nudges, ADR 0014 — a one-shot reaction, not a live aggregate.)
How to verify the rule still holds
tests/test_copilot_pending.py— the endpoint runs and reuses a single session across calls.tests/test_module_isolation.py— copilot declares nodependsand imports no sibling module.- grep:
PendingServicecalls onlytool_registry, neverfrom app.modules.<other>.
References
backend/app/modules/copilot/service.py(PendingService)backend/app/modules/copilot/router.py(GET /pending)- ADR 0001 (modular contract), ADR 0003 (event bus over imports), ADR 0014 (proactivity / nudges)
docs/technical/copilot/ia-redesign-plan.md(Fase 2)