mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user