fix(learning): never broadcast agent learnings to human roles (#250)

Every recorded learning was sent as a knowledge-share notification to all agents,
and the recipient query included the human / human-driven roles (CEO, prompter,
secretary) — so the CEO's inbox filled with agent learnings. Exclude those roles
from the recipient query. The human-role set is resolved from the foundation enum
at import (a module constant) so a test that patches the models.base AgentRole
alias can't break it.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-06-24 03:35:02 +02:00
committed by GitHub
co-authored by Renn F
parent 40d685bd9f
commit d8254300cd
3 changed files with 52 additions and 4 deletions
+2
View File
@@ -18,6 +18,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- **A failed PR review no longer looks green.** On a task's detail page, the "PR Reviewer Notes" card was painted a fixed teal/green background regardless of the review verdict, so a `Failed` review — red badge and all — sat inside a green card and could read as passing at a glance. The card background now mirrors the verdict the way the QA Notes card already does: red on a failed review, green on approved/passed, amber on changes-requested, and neutral before a verdict is in.
- **The CEO and other human roles no longer get spammed with agent "learnings."** Whenever an agent recorded a learning, RoboCo broadcast it as a knowledge-share notification — and the recipient query swept in the human roles too (the CEO, plus the human-driven prompter and secretary). Agent knowledge-sharing is a signal for *agents*; in a human's inbox it is just noise. Those roles are now excluded from learning broadcasts.
## [0.10.0] - 2026-06-23
### Added
+15 -4
View File
@@ -16,8 +16,16 @@ from uuid import UUID
import structlog
from roboco.foundation.identity import Role as _Role
from roboco.models.optimal import IndexType, SearchResult
# Human / human-driven roles are never agent learning recipients: the CEO is the
# human operator, and the prompter (intake) + secretary act only under direct CEO
# command, so a knowledge-share ping to them is inbox noise, not an agent signal.
# Resolved from the foundation enum at import time so a test that patches the
# models.base AgentRole alias can't break this constant.
_HUMAN_ONLY_ROLES = (_Role.CEO, _Role.PROMPTER, _Role.SECRETARY)
logger = structlog.get_logger()
@@ -201,18 +209,21 @@ class LearningPropagationService:
from roboco.db.base import get_db_context
from roboco.db.tables import AgentTable
from roboco.models import NotificationPriority, NotificationType
from roboco.models.base import AgentRole
from roboco.models.notification import CreateNotificationParams
from roboco.services.notification import NotificationService
try:
async with get_db_context() as db:
# Build query based on scope
query = select(AgentTable).where(AgentTable.id != learning.agent_id)
# Build query based on scope; never the author, never a human role.
query = (
select(AgentTable)
.where(AgentTable.id != learning.agent_id)
.where(AgentTable.role.notin_(_HUMAN_ONLY_ROLES))
)
if learning.scope == LearningScope.TEAM:
# Only notify agents with the same role
from roboco.models.base import AgentRole
try:
role_enum = AgentRole(learning.agent_role.upper())
query = query.where(AgentTable.role == role_enum)
@@ -120,6 +120,41 @@ async def test_team_scope_role_uppercase_via_agent_role_enum(
assert other_role.id in notified_ids
@pytest.mark.asyncio
async def test_org_scope_excludes_human_roles(
shared_session: AsyncSession,
) -> None:
"""Human / human-driven roles are never learning recipients, even at ORG
scope: knowledge-share is a signal for agents, not noise in the human's inbox.
"""
author = _make_agent(AgentRole.DEVELOPER)
peer = _make_agent(AgentRole.QA)
ceo = _make_agent(AgentRole.CEO)
secretary = _make_agent(AgentRole.SECRETARY)
shared_session.add_all([author, peer, ceo, secretary])
await shared_session.flush()
svc = LearningPropagationService()
await svc.initialize(_StubOptimal())
learning = await svc.record_learning(
RecordLearningParams(
agent_id=cast("uuid.UUID", author.id),
agent_role="developer",
content="A useful pattern worth sharing org-wide",
learning_type=LearningType.PATTERN,
scope=LearningScope.ORG,
)
)
notified_ids = {
n.target_agent_id
for n in svc._notification_queue
if n.learning_id == learning.learning_id
}
assert peer.id in notified_ids
assert ceo.id not in notified_ids
assert secretary.id not in notified_ids
@pytest.mark.asyncio
async def test_team_scope_invalid_role_skips_filter(
shared_session: AsyncSession,