diff --git a/agents/inbox/2026-08-26-antigravity-release-gate-a30.md b/agents/inbox/2026-08-26-antigravity-release-gate-a30.md new file mode 100644 index 0000000..67c836f --- /dev/null +++ b/agents/inbox/2026-08-26-antigravity-release-gate-a30.md @@ -0,0 +1,43 @@ +# Задание Antigravity: полный release gate после A30 + +## Цель + +Провести независимую проверку ветки `codex/workflow-canvas` после реализации A30. Проверять фактическое состояние репозитория и запускаемого приложения, а не описание работы. + +## Обязательный порядок + +1. Получить актуальные `origin/main` и `origin/codex/workflow-canvas`. +2. Проверить `git status`, базовый и финальный SHA ветки. +3. Запустить приложение из чистого checkout ветки A30. +4. Выполнить полный `pytest`/release gate и сохранить полный вывод. +5. Выполнить `ruff check .`. +6. Проверить веб-контракт: `/`, `/api/snapshot`, `/api/events`, `/api/action`. +7. Проверить A30 вручную в браузере: LIVE, EDIT, создание агента, назначение Provider → Account → Model, Agent File, редактор ребра, цикл и предел итераций. +8. Отдельно проверить честность данных: отсутствие mock/demo чисел из макета, `Н/Д` с причиной, loading не смешан с отсутствием данных. +9. Проверить persistence после перезапуска и реальные provider errors. +10. Проверить, что desktop `router/ui/**` не изменён A30. + +## Правила отчёта + +- Не писать `PASS`, если полный release gate не запускался. +- Не считать targeted tests заменой полного regression. +- Для каждого failure привести команду, stdout/stderr, файл и минимальный способ воспроизведения. +- Если блокер связан с окружением, повторить проверку в чистом окружении или явно указать, что именно не проверено. + +## Артефакты + +Передать: + +- `START_HEAD`, `FINAL_HEAD`, `origin/main`; +- чистый `git status` или полный список загрязнений; +- `X passed / Y skipped / Z failed`; +- точный результат `scripts/release_gate.py`; +- список найденных дефектов с приоритетом P0–P3; +- скриншоты LIVE, EDIT, Inspector, Agent File и редактора ребра; +- отдельный список пропущенных проверок. + +## Ограничения + +- Ничего не исправлять молча в чужой ветке: найденные дефекты оформить отдельным патчем/коммитом или вернуть владельцу. +- Не удалять пользовательские изменения в установщике, бинарниках и заданиях inbox. +- Не объявлять release-ready при известных блокерах. diff --git a/docs/screenshots/a30/agent-file-editor.png b/docs/screenshots/a30/agent-file-editor.png new file mode 100644 index 0000000..3c6d244 Binary files /dev/null and b/docs/screenshots/a30/agent-file-editor.png differ diff --git a/docs/screenshots/a30/edge-editor.png b/docs/screenshots/a30/edge-editor.png new file mode 100644 index 0000000..ff7a881 Binary files /dev/null and b/docs/screenshots/a30/edge-editor.png differ diff --git a/docs/screenshots/a30/overview-edit-inspector.png b/docs/screenshots/a30/overview-edit-inspector.png new file mode 100644 index 0000000..3b820f1 Binary files /dev/null and b/docs/screenshots/a30/overview-edit-inspector.png differ diff --git a/docs/screenshots/a30/overview-live-provider-error.png b/docs/screenshots/a30/overview-live-provider-error.png new file mode 100644 index 0000000..d6410c4 Binary files /dev/null and b/docs/screenshots/a30/overview-live-provider-error.png differ diff --git a/docs/screenshots/a30/overview-live.png b/docs/screenshots/a30/overview-live.png new file mode 100644 index 0000000..206011f Binary files /dev/null and b/docs/screenshots/a30/overview-live.png differ diff --git a/docs/web-api/CONTRACT.md b/docs/web-api/CONTRACT.md index 68579fc..542088b 100644 --- a/docs/web-api/CONTRACT.md +++ b/docs/web-api/CONTRACT.md @@ -90,6 +90,24 @@ readiness, agents, providers, routing, quotas, metrics, is_stale Имена действий берутся **ровно** из общего слоя `action_handler.py`: +Действия Agent Manager и Workflow (A30): + +| Action | Назначение | Обязательные данные | +|---|---|---| +| `create_agent` | Создать логического агента, роль маршрутизатора и Agent File | `name`, `role`; опционально `profile_id`, `model`, настройки исполнения | +| `update_agent` | Изменить свойства и назначение Provider → Account → Model | `agent_id`; назначение задаётся `provider`, `profile_id`, `model` | +| `delete_agent` | Удалить агента; при ссылках сначала возвращает `confirmation_required` | `agent_id`; после подтверждения `force: true` | +| `read_agent_file` | Прочитать реальный Markdown Agent File | `agent_id` | +| `save_agent_file` | Атомарно сохранить Agent File для последующих запусков | `agent_id`, `content` | +| `save_workflow` | Валидировать и сохранить узлы, рёбра, layout и предел итераций | `edges`, `agents`, `max_iterations` | +| `start_workflow` | Запустить реальную задачу через RouterEngine | `task` | +| `stop_workflow` | Запросить остановку текущего запуска | — | + +Состояние графа и LIVE-журнал приходят в поле `workflow` ответа +`GET /api/snapshot`. `workflow.run.status=loading` означает загрузку; +`unavailable_reason` означает отсутствие данных с явной причиной. Показатели +workflow нельзя подменять фикстурой при недоступности API. + ``` account_details add_account agent_settings apply_update assign_role auto_assign_all check_updates delete_credentials diff --git a/src/antigravity_provider/paths.py b/src/antigravity_provider/paths.py index 124869f..5088675 100644 --- a/src/antigravity_provider/paths.py +++ b/src/antigravity_provider/paths.py @@ -85,6 +85,23 @@ def get_router_active_profile_path() -> Path: return get_config_dir() / "router_active_profile.json" +def get_workflow_state_path() -> Path: + """Return the persisted agent/workflow state sidecar. + + Logical roles and their execution routes remain canonical in + ``router_profiles.yaml``. This file stores only the extra agent metadata, + graph layout and execution checkpoints which do not belong to routing. + """ + return get_config_dir() / "workflow_state.json" + + +def get_agent_files_dir() -> Path: + """Return the user-editable directory containing real Agent Files.""" + directory = get_hermes_home() / "agents" + directory.mkdir(parents=True, exist_ok=True) + return directory + + def get_compatibility_path() -> Path: return get_config_dir() / "compatibility.json" diff --git a/src/antigravity_provider/router/action_handler.py b/src/antigravity_provider/router/action_handler.py index 1366dc8..d9f2aba 100644 --- a/src/antigravity_provider/router/action_handler.py +++ b/src/antigravity_provider/router/action_handler.py @@ -305,6 +305,23 @@ class ActionExecutor: except Exception: pass + if action in { + 'create_agent', + 'update_agent', + 'delete_agent', + 'read_agent_file', + 'save_agent_file', + 'save_workflow', + 'start_workflow', + 'stop_workflow', + }: + try: + from antigravity_provider.router.workflow_service import execute_workflow_action + + return execute_workflow_action(action, data) + except (ValueError, OSError) as exc: + return {'ok': False, 'message': str(exc)} + # Device-flow для Grok и Codex через веб. Backend был готов давно, но # наружу не выведен: веб-мастер показывал заглушку «не реализовано», и # подключить эти провайдеры можно было только из десктопа. Настоящие diff --git a/src/antigravity_provider/router/state_store.py b/src/antigravity_provider/router/state_store.py index 2c3550f..2cb1ac4 100644 --- a/src/antigravity_provider/router/state_store.py +++ b/src/antigravity_provider/router/state_store.py @@ -55,6 +55,7 @@ class HubSnapshot: quotas: Dict[str, Any] metrics: Dict[str, Any] = field(default_factory=dict) is_stale: bool = False + workflow: Dict[str, Any] = field(default_factory=dict) def get_profile(self, profile_id: str) -> Optional[ProfileViewModel]: return self.all_profiles.get(profile_id) @@ -107,6 +108,24 @@ class HubStateStore: if self._current_snapshot is not None: if (time.time() - self._current_snapshot.timestamp > 300.0) and not self._current_snapshot.is_stale: self._current_snapshot = replace(self._current_snapshot, is_stale=True) + # Provider/account scans are intentionally cached, while LIVE + # workflow checkpoints are small local state and must never lag + # behind an action until the next expensive provider refresh. + try: + from .workflow_service import WorkflowService + + live_workflow = WorkflowService.get().snapshot() + role_views = {agent.role_id: agent for agent in self._current_snapshot.agents} + for workflow_agent in live_workflow.get("agents", []): + role_view = role_views.get(workflow_agent.get("role")) + generic_name = str(workflow_agent.get("role") or "").replace("-", " ").title() + if role_view and workflow_agent.get("name") == generic_name: + workflow_agent["name"] = role_view.role_name_ru + if role_view and not workflow_agent.get("description"): + workflow_agent["description"] = role_view.role_description_ru + self._current_snapshot = replace(self._current_snapshot, workflow=live_workflow) + except Exception: + pass return self._current_snapshot return self.refresh(force_scan=False) @@ -196,6 +215,27 @@ class HubStateStore: "active_calls_total": active_leases_total, "active_calls_by_profile": active_leases_by_profile, } + try: + from .workflow_service import WorkflowService + + workflow_data = WorkflowService.get().snapshot() + role_views = {agent.role_id: agent for agent in agents} + for workflow_agent in workflow_data.get("agents", []): + role_view = role_views.get(workflow_agent.get("role")) + generic_name = str(workflow_agent.get("role") or "").replace("-", " ").title() + if role_view and workflow_agent.get("name") == generic_name: + workflow_agent["name"] = role_view.role_name_ru + if role_view and not workflow_agent.get("description"): + workflow_agent["description"] = role_view.role_description_ru + except Exception as exc: + workflow_data = { + "agents": [], + "definition": {}, + "run": {"status": "unavailable"}, + "events": [], + "is_loading": False, + "unavailable_reason": f"Workflow state unavailable: {exc}", + } snapshot = HubSnapshot( generation=gen, seq=request_seq, @@ -209,6 +249,7 @@ class HubStateStore: quotas=quotas_map, metrics=metrics, is_stale=False, + workflow=workflow_data, ) self._current_snapshot = snapshot @@ -263,6 +304,13 @@ class HubStateStore: "active_calls_by_profile": {}, }, is_stale=True, + workflow={ + "agents": [], + "definition": {}, + "run": {"status": "loading"}, + "events": [], + "is_loading": True, + }, ) def _apply_profile_delta(self, profile: ProfileViewModel) -> HubSnapshot: diff --git a/src/antigravity_provider/router/web/static/app.js b/src/antigravity_provider/router/web/static/app.js index e97bfd8..9b42033 100644 --- a/src/antigravity_provider/router/web/static/app.js +++ b/src/antigravity_provider/router/web/static/app.js @@ -519,7 +519,7 @@ function updateGlobalHeader() { const isHealthy = readiness.state === 'healthy'; const readyRoles = readiness.roles_ready_count || 0; - const totalRoles = readiness.total_roles || 6; + const totalRoles = readiness.total_roles ?? 0; if (elements.headerReadinessBadge) { elements.headerReadinessBadge.className = `header-readiness-badge ${isHealthy ? 'text-healthy' : 'text-warning'}`; @@ -527,7 +527,7 @@ function updateGlobalHeader() { if (elements.headerReadinessText) { elements.headerReadinessText.textContent = readiness.title_ru ? `${readiness.title_ru} (${readyRoles}/${totalRoles} ролей)` - : 'Система готова'; + : 'Н/Д: состояние ещё не измерено'; } const kpiReadiness = document.getElementById('kpi-system-readiness'); @@ -805,6 +805,10 @@ function renderQuotaCell(bucket, unavailableReason) { // 1. OVERVIEW VIEW (P0-3, P0-4 Diagram Model Select & Counters) // ═══════════════════════════════════════════════════════════════ function renderOverviewView() { + if (typeof renderWorkflowOverview === 'function') { + renderWorkflowOverview(currentSnapshot); + return; + } if (!currentSnapshot) return; const providers = currentSnapshot.providers || []; diff --git a/src/antigravity_provider/router/web/static/index.html b/src/antigravity_provider/router/web/static/index.html index d9cf4a5..ec1aa73 100644 --- a/src/antigravity_provider/router/web/static/index.html +++ b/src/antigravity_provider/router/web/static/index.html @@ -5,6 +5,7 @@