From 74a901d89f5cd96eb1e45ec7c3c6cb86cda0b9a2 Mon Sep 17 00:00:00 2001 From: vladmesh <16962535+vladmesh@users.noreply.github.com> Date: Sat, 18 Jul 2026 20:00:18 +0300 Subject: [PATCH] feat: let PO select developer worker --- docs/coding-agents.md | 6 +++++ .../langgraph/src/agents/po/tools_projects.py | 19 ++++++++++++++- .../langgraph/tests/unit/po/test_tools.py | 23 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/docs/coding-agents.md b/docs/coding-agents.md index d41b4e1c..55005de1 100644 --- a/docs/coding-agents.md +++ b/docs/coding-agents.md @@ -88,6 +88,12 @@ documentation for the upstream behavior. Developer node в Engineering Subgraph использует coding agents через `worker-manager` сервис (PO не использует контейнеры — это LangGraph ReactAgent): +При создании проекта PO передаёт выбранного developer-воркера в +`create_project(agent_type="claude" | "factory" | "codex")`. Значение сохраняется +в `project.config.agent_type` и действует для engineering-задач этого проекта. +Если выбор не указан, используется `claude`. Неизвестное значение отклоняется до +создания проекта. + 1. Worker-manager создаёт контейнер из worker-base образа 2. Монтирует pre-scaffolded workspace (`/data/workspaces/{repo_id}/`) — код уже на месте 3. Worker-manager creates/checks out story feature branch (`story/{story_id}`) diff --git a/services/langgraph/src/agents/po/tools_projects.py b/services/langgraph/src/agents/po/tools_projects.py index d2b5fa45..2e888516 100644 --- a/services/langgraph/src/agents/po/tools_projects.py +++ b/services/langgraph/src/agents/po/tools_projects.py @@ -11,6 +11,7 @@ import structlog from shared.contracts.dto.project import ProjectStatus, ServiceModule +from shared.contracts.vocab import AgentType from .tools_shared import _get_api, _user_headers @@ -23,6 +24,11 @@ # Available modules (single source of truth: ServiceModule enum) # --------------------------------------------------------------------------- AVAILABLE_MODULES = {m.value for m in ServiceModule} +AVAILABLE_DEVELOPER_AGENTS = { + AgentType.CLAUDE.value, + AgentType.FACTORY.value, + AgentType.CODEX.value, +} HTTP_OK = 200 TELEGRAM_API_TIMEOUT = 10 @@ -33,6 +39,7 @@ async def create_project( name: str, modules: str = "backend", description: str = "", + agent_type: str = AgentType.CLAUDE.value, *, config: RunnableConfig, ) -> str: @@ -42,6 +49,7 @@ async def create_project( name: Project name (lowercase, starts with letter, only a-z/0-9/hyphens). modules: Comma-separated modules: backend, tg_bot, notifications, frontend. description: What the project should do. + agent_type: Developer worker: claude, factory, or codex. """ modules_list = [m.strip() for m in modules.split(",") if m.strip()] @@ -50,11 +58,20 @@ async def create_project( available = ", ".join(sorted(AVAILABLE_MODULES)) return f"Error: invalid modules: {', '.join(invalid)}. Available: {available}" + if agent_type not in AVAILABLE_DEVELOPER_AGENTS: + available = ", ".join(sorted(AVAILABLE_DEVELOPER_AGENTS)) + return f"Error: invalid agent_type: {agent_type}. Available: {available}" + if "backend" not in modules_list: modules_list.insert(0, "backend") project_id = str(uuid.uuid4()) - proj_config = {"modules": modules_list, "description": description, "name": name} + proj_config = { + "modules": modules_list, + "description": description, + "name": name, + "agent_type": agent_type, + } payload = { "id": project_id, diff --git a/services/langgraph/tests/unit/po/test_tools.py b/services/langgraph/tests/unit/po/test_tools.py index 26e58007..71696da3 100644 --- a/services/langgraph/tests/unit/po/test_tools.py +++ b/services/langgraph/tests/unit/po/test_tools.py @@ -86,6 +86,29 @@ async def test_creates_project_with_modules(self, mock_api_client): assert "Project created" in result assert "abc123" in result + @pytest.mark.asyncio + @pytest.mark.parametrize("agent_type", ["claude", "factory", "codex"]) + async def test_persists_selected_developer_agent(self, mock_api_client, agent_type): + mock_api_client.post.return_value = _make_response({"id": "x", "name": "project"}) + + await create_project.ainvoke( + {"name": "project", "modules": "backend", "agent_type": agent_type}, + config=_make_config("user-1"), + ) + + project_call = mock_api_client.post.call_args_list[0] + assert project_call[1]["json"]["config"]["agent_type"] == agent_type + + @pytest.mark.asyncio + async def test_rejects_unknown_developer_agent(self, mock_api_client): + result = await create_project.ainvoke( + {"name": "project", "modules": "backend", "agent_type": "mystery"}, + config=_make_config("user-1"), + ) + + assert result == "Error: invalid agent_type: mystery. Available: claude, codex, factory" + mock_api_client.post.assert_not_called() + @pytest.mark.asyncio async def test_passes_telegram_id_header(self, mock_api_client): mock_api_client.post.return_value = _make_response({"id": "x", "name": "y"})