mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
1. Blueprint updates - Added NO_GROUPS escalation docs to all 12 agent blueprints
2. Multi-image Docker architecture - Created role-specific Dockerfiles: - agent-base.Dockerfile (shared foundation) - agent-pm.Dockerfile, agent-dev-be.Dockerfile, agent-dev-fe.Dockerfile - agent-qa-be.Dockerfile, agent-qa-fe.Dockerfile, agent-doc.Dockerfile, agent-ux.Dockerfile 3. Orchestrator updates - roboco/runtime/orchestrator.py: - Added AGENT_IMAGES mapping and get_agent_image() function - Updated _ensure_agent_image() to build base + specialized images - Updated _spawn_container() to use role-specific image - Made _generate_mcp_config() role-aware (though kept notify for all since they need to receive)
This commit is contained in:
@@ -18,7 +18,7 @@ from typing import Any
|
||||
from fastapi import status
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from roboco.agents_config import CHANNEL_ACCESS
|
||||
from roboco.agents_config import CHANNEL_ACCESS, get_agent_role
|
||||
from roboco.llm import ToonAdapter
|
||||
from roboco.mcp.schemas import (
|
||||
AskQuestionInput,
|
||||
@@ -40,6 +40,36 @@ _toon = ToonAdapter()
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _format_no_groups_error(agent_id: str, channel_slug: str) -> dict[str, Any]:
|
||||
"""Format NO_GROUPS error with role-aware escalation guidance."""
|
||||
role = get_agent_role(agent_id)
|
||||
|
||||
if role == "main_pm":
|
||||
guidance = (
|
||||
f"Channel #{channel_slug} has no groups. "
|
||||
"Use roboco_group_create to create a group for this channel."
|
||||
)
|
||||
elif role == "cell_pm":
|
||||
guidance = (
|
||||
f"Channel #{channel_slug} has no groups. "
|
||||
"Groups are created by Main PM. "
|
||||
"Use roboco_task_escalate to request group creation."
|
||||
)
|
||||
else:
|
||||
# Developer/QA/Documenter
|
||||
guidance = (
|
||||
f"Channel #{channel_slug} has no groups yet. "
|
||||
"Escalate to your Cell PM. "
|
||||
"If you have a task_id, include it in your message call."
|
||||
)
|
||||
|
||||
return format_error_response(
|
||||
"NO_GROUPS",
|
||||
f"Channel #{channel_slug} has no groups.",
|
||||
{"guidance": guidance, "channel": channel_slug, "role": role},
|
||||
)
|
||||
|
||||
|
||||
def _check_channel_access(agent_id: str, channel_slug: str, action: str) -> bool:
|
||||
"""Check if agent has access to channel for the given action."""
|
||||
channel = CHANNEL_ACCESS.get(channel_slug, {})
|
||||
@@ -102,8 +132,14 @@ def _validate_message_send(
|
||||
async def _get_default_group(
|
||||
client: ApiClient,
|
||||
channel_id: str,
|
||||
agent_id: str | None = None,
|
||||
channel_slug: str | None = None,
|
||||
) -> str | dict[str, Any]:
|
||||
"""Get the default (first) group for a channel. Returns group_id or error dict."""
|
||||
"""Get the default (first) group for a channel. Returns group_id or error dict.
|
||||
|
||||
If agent_id and channel_slug are provided, NO_GROUPS error includes
|
||||
role-aware escalation guidance.
|
||||
"""
|
||||
resp = await client.get(f"/channels/{channel_id}/groups")
|
||||
|
||||
if not resp.ok:
|
||||
@@ -115,6 +151,9 @@ async def _get_default_group(
|
||||
|
||||
groups = resp.json()
|
||||
if not groups:
|
||||
# Return role-aware guidance if we have context
|
||||
if agent_id and channel_slug:
|
||||
return _format_no_groups_error(agent_id, channel_slug)
|
||||
return format_error_response("NO_GROUPS", "Channel has no groups")
|
||||
|
||||
# Return first active group, or first group if none are active
|
||||
@@ -127,6 +166,8 @@ async def _get_default_group(
|
||||
async def _get_active_session(
|
||||
client: ApiClient,
|
||||
channel_id: str,
|
||||
agent_id: str | None = None,
|
||||
channel_slug: str | None = None,
|
||||
) -> str | dict[str, Any]:
|
||||
"""Get active session for channel. Returns session_id or error dict.
|
||||
|
||||
@@ -135,9 +176,11 @@ async def _get_active_session(
|
||||
returns an error guiding the agent to use task_id or ask their PM.
|
||||
"""
|
||||
# First get the default group for this channel
|
||||
group_result = await _get_default_group(client, channel_id)
|
||||
group_result = await _get_default_group(
|
||||
client, channel_id, agent_id=agent_id, channel_slug=channel_slug
|
||||
)
|
||||
if isinstance(group_result, dict):
|
||||
return group_result # Error response
|
||||
return group_result # Error response (NO_GROUPS with guidance)
|
||||
group_id = group_result
|
||||
|
||||
# Check if group has an active session
|
||||
@@ -281,8 +324,10 @@ async def _handle_channel_history(
|
||||
return channel_result
|
||||
channel_id = channel_result
|
||||
|
||||
# Get group
|
||||
group_result = await _get_default_group(client, channel_id)
|
||||
# Get group (with role-aware guidance if NO_GROUPS)
|
||||
group_result = await _get_default_group(
|
||||
client, channel_id, agent_id=agent_id, channel_slug=channel_slug
|
||||
)
|
||||
if isinstance(group_result, dict):
|
||||
return group_result
|
||||
group_id = group_result
|
||||
@@ -366,9 +411,11 @@ async def _handle_message_send(
|
||||
return channel_result # Error response
|
||||
channel_id = channel_result
|
||||
|
||||
session_result = await _get_active_session(client, channel_id)
|
||||
session_result = await _get_active_session(
|
||||
client, channel_id, agent_id=agent_id, channel_slug=data.channel_slug
|
||||
)
|
||||
if isinstance(session_result, dict):
|
||||
return session_result # Returns NO_ACTIVE_SESSION error with guidance
|
||||
return session_result # Returns NO_GROUPS/NO_ACTIVE_SESSION with guidance
|
||||
session_id = session_result
|
||||
|
||||
# Resolve mentions (slugs) to UUIDs using shared cache
|
||||
|
||||
@@ -258,3 +258,35 @@ class SessionLinkTaskInput(BaseModel):
|
||||
default="discussion",
|
||||
description="Type: discussion, planning, review, retrospective",
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GROUP SCHEMAS (Main PM Only)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class GroupCreateInput(BaseModel):
|
||||
"""Input for creating a group in a channel (Main PM only).
|
||||
|
||||
Groups organize work into feature/initiative scopes within channels.
|
||||
- Main PM creates Groups for features/initiatives
|
||||
- Cell PMs create Sessions within Groups for work items
|
||||
- Developers communicate within Sessions
|
||||
"""
|
||||
|
||||
channel_slug: str = Field(
|
||||
...,
|
||||
description="Channel slug where group will be created (e.g., 'backend-cell')",
|
||||
)
|
||||
name: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
max_length=100,
|
||||
description="Group name (e.g., 'User Preferences Feature')",
|
||||
)
|
||||
hierarchy_level: int = Field(
|
||||
default=4,
|
||||
ge=0,
|
||||
le=4,
|
||||
description="Access level: 0=CEO, 1=Board, 2=Main PM, 3=Cell PM, 4=Members",
|
||||
)
|
||||
|
||||
@@ -34,6 +34,7 @@ from typing import Any
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from roboco.mcp.schemas import (
|
||||
GroupCreateInput,
|
||||
SessionCreateForTasksInput,
|
||||
SessionLinkTaskInput,
|
||||
TaskAssignInput,
|
||||
@@ -45,6 +46,7 @@ from roboco.mcp.schemas import (
|
||||
from roboco.mcp.tasks.handlers import (
|
||||
handle_agent_idle,
|
||||
handle_docs_complete,
|
||||
handle_group_create,
|
||||
handle_session_create_for_tasks,
|
||||
handle_session_get_for_task,
|
||||
handle_session_link_task,
|
||||
@@ -656,6 +658,29 @@ def _register_session_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> N
|
||||
"""
|
||||
return await handle_session_get_for_task(client, task_id, agent_id)
|
||||
|
||||
@mcp.tool()
|
||||
async def roboco_group_create(data: GroupCreateInput) -> dict[str, Any]:
|
||||
"""
|
||||
Create a group in a channel (Main PM only).
|
||||
|
||||
Groups organize work into feature/initiative scopes within channels.
|
||||
The typical workflow is:
|
||||
1. Main PM creates a Group for a feature/initiative
|
||||
2. Cell PM creates Sessions within the Group for work items
|
||||
3. Developers communicate within Sessions
|
||||
|
||||
ENFORCEMENT:
|
||||
- Only Main PM, CEO, or Auditor can create groups
|
||||
- Cell PMs should escalate if they need a group created
|
||||
|
||||
Args:
|
||||
data: GroupCreateInput with channel_slug, name, hierarchy_level
|
||||
|
||||
Returns:
|
||||
Created group with guidance
|
||||
"""
|
||||
return await handle_group_create(client, data, agent_id)
|
||||
|
||||
|
||||
def create_task_mcp_server(agent_id: str) -> FastMCP:
|
||||
"""
|
||||
|
||||
@@ -30,6 +30,7 @@ from roboco.mcp.tasks.handlers.review import (
|
||||
)
|
||||
from roboco.mcp.tasks.handlers.scan import handle_task_get, handle_task_scan
|
||||
from roboco.mcp.tasks.handlers.sessions import (
|
||||
handle_group_create,
|
||||
handle_session_create_for_tasks,
|
||||
handle_session_get_for_task,
|
||||
handle_session_link_task,
|
||||
@@ -44,6 +45,7 @@ from roboco.mcp.tasks.handlers.work import (
|
||||
__all__ = [
|
||||
"handle_agent_idle",
|
||||
"handle_docs_complete",
|
||||
"handle_group_create",
|
||||
"handle_session_create_for_tasks",
|
||||
"handle_session_get_for_task",
|
||||
"handle_session_link_task",
|
||||
|
||||
@@ -258,3 +258,86 @@ async def handle_session_get_for_task(
|
||||
"primary_session_id": primary.get("session_id") if primary else None,
|
||||
"guidance": guidance,
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GROUP HANDLERS (Main PM Only)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _validate_main_pm_permissions(agent_id: str) -> dict[str, Any] | None:
|
||||
"""Validate agent has Main PM permissions (for group creation).
|
||||
|
||||
Only Main PM, CEO, and Auditor can create groups.
|
||||
Cell PMs should escalate to Main PM for group creation.
|
||||
"""
|
||||
role = get_agent_role(agent_id)
|
||||
allowed_roles = {"main_pm", "ceo", "auditor"}
|
||||
if role not in allowed_roles:
|
||||
return format_error_response(
|
||||
"PERMISSION_DENIED",
|
||||
"Only Main PM can create groups. Cell PMs should escalate.",
|
||||
{
|
||||
"role": role,
|
||||
"guidance": "Use roboco_task_escalate to request group creation.",
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
async def handle_group_create(
|
||||
client: ApiClient,
|
||||
input_data: Any, # GroupCreateInput from roboco.mcp.schemas
|
||||
agent_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Handle group creation (Main PM only).
|
||||
|
||||
Groups organize work into feature/initiative scopes within channels.
|
||||
Cell PMs then create sessions within groups for work items.
|
||||
"""
|
||||
if error := _validate_main_pm_permissions(agent_id):
|
||||
return error
|
||||
|
||||
payload = {
|
||||
"channel_slug": input_data.channel_slug,
|
||||
"name": input_data.name,
|
||||
"hierarchy_level": input_data.hierarchy_level,
|
||||
}
|
||||
|
||||
try:
|
||||
resp = await client.post("/groups", json=payload)
|
||||
except Exception as e:
|
||||
return format_error_response(
|
||||
"CONNECTION_ERROR",
|
||||
f"Failed to connect to API: {type(e).__name__}",
|
||||
)
|
||||
|
||||
if resp.is_status(status.HTTP_403_FORBIDDEN):
|
||||
return format_error_response(
|
||||
"PERMISSION_DENIED",
|
||||
"API rejected group creation",
|
||||
{"detail": resp.text},
|
||||
)
|
||||
|
||||
if resp.is_status(status.HTTP_404_NOT_FOUND):
|
||||
return format_error_response(
|
||||
"NOT_FOUND",
|
||||
f"Channel not found: {input_data.channel_slug}",
|
||||
)
|
||||
|
||||
if not resp.is_status(status.HTTP_201_CREATED):
|
||||
return format_error_response(
|
||||
"CREATE_FAILED",
|
||||
"Failed to create group",
|
||||
{"status_code": resp.status_code, "detail": resp.text},
|
||||
)
|
||||
|
||||
group_data = resp.json()
|
||||
return {
|
||||
"status": "created",
|
||||
"group": group_data,
|
||||
"guidance": (
|
||||
f"Group '{input_data.name}' created in #{input_data.channel_slug}. "
|
||||
"Cell PMs can now create sessions within this group."
|
||||
),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user