Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/coding-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand Down
19 changes: 18 additions & 1 deletion services/langgraph/src/agents/po/tools_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -33,6 +39,7 @@ async def create_project(
name: str,
modules: str = "backend",
description: str = "",
agent_type: str = AgentType.CLAUDE.value,
*,
config: RunnableConfig,
) -> str:
Expand All @@ -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()]

Expand All @@ -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,
Expand Down
23 changes: 23 additions & 0 deletions services/langgraph/tests/unit/po/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Expand Down
Loading