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. - **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 ## [0.10.0] - 2026-06-23
### Added ### Added
+15 -4
View File
@@ -16,8 +16,16 @@ from uuid import UUID
import structlog import structlog
from roboco.foundation.identity import Role as _Role
from roboco.models.optimal import IndexType, SearchResult 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() logger = structlog.get_logger()
@@ -201,18 +209,21 @@ class LearningPropagationService:
from roboco.db.base import get_db_context from roboco.db.base import get_db_context
from roboco.db.tables import AgentTable from roboco.db.tables import AgentTable
from roboco.models import NotificationPriority, NotificationType from roboco.models import NotificationPriority, NotificationType
from roboco.models.base import AgentRole
from roboco.models.notification import CreateNotificationParams from roboco.models.notification import CreateNotificationParams
from roboco.services.notification import NotificationService from roboco.services.notification import NotificationService
try: try:
async with get_db_context() as db: async with get_db_context() as db:
# Build query based on scope # Build query based on scope; never the author, never a human role.
query = select(AgentTable).where(AgentTable.id != learning.agent_id) query = (
select(AgentTable)
.where(AgentTable.id != learning.agent_id)
.where(AgentTable.role.notin_(_HUMAN_ONLY_ROLES))
)
if learning.scope == LearningScope.TEAM: if learning.scope == LearningScope.TEAM:
# Only notify agents with the same role # Only notify agents with the same role
from roboco.models.base import AgentRole
try: try:
role_enum = AgentRole(learning.agent_role.upper()) role_enum = AgentRole(learning.agent_role.upper())
query = query.where(AgentTable.role == role_enum) 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 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 @pytest.mark.asyncio
async def test_team_scope_invalid_role_skips_filter( async def test_team_scope_invalid_role_skips_filter(
shared_session: AsyncSession, shared_session: AsyncSession,