# Балда / Валера — эксплуатация ## Расположение - Repo: `~/Developer/balda/` (ветка `main`) - Code: upstream `normahq/balda` + `feat/zulip-transport` влит, `normahq/norma` v0.0.10 - **Рабочий compose**: `~/Docker/balda-agent/docker-compose.yaml` → контейнер `balda-agent-valera-1` - Config (persistent): `~/Docker/balda-agent/.config/balda/config.yaml` - Env: `~/Docker/balda-agent/.env` - Owner: `allowed_owners` в config.yaml (статически, `/start owner=` не нужен) ## Запуск / рестарт ```bash cd ~/Docker/balda-agent docker-compose restart # без пересборки, только конфиг/env не менялись docker-compose up -d # пересоздать контейнер, перечитать .env # Rebuild после изменений в коде: cd ~/Docker/claudio-agent && docker-compose build docker tag claudio-agent-claudio:latest balda-agent-valera:latest cd ~/Docker/claudio-agent && docker-compose up -d cd ~/Docker/balda-agent && docker-compose up -d ``` > Валера и Клавдий — **один образ** (`balda-agent-valera`), собирается из одного Dockerfile (`Dockerfile.claudio`). Разница только в конфиге (`config.yaml`) и `.env`, которые монтируются volumes. Сборка идёт из `~/Docker/claudio-agent/` — там есть build секция. ## Архитектура получения сообщений Валера — **outgoing webhook bot** (bot_type=3). Zulip отправляет webhook только для @mention и DM. - `events_polling.enabled: false` в config.yaml - Сообщения приходят только через **outgoing webhook** (webhook_token в .env) - После @mention создаётся сессия; последующие сообщения без @mention обрабатываются **После перезапуска**: написать `@Валера <текст>` чтобы создать сессию. ## MCP Obsidian — конфиг Balda через SSE подключается к obsidian-mcp. **URL обязательно с `/sse`**: ```yaml runtime: mcp_servers: obsidian: type: sse url: http://obsidian-mcp:3101/sse # <-- без /sse → 404 Not Found name: obsidian ``` ## Контекст (история сообщений) **Работает** (на upstream `normahq/balda` с поддержкой hosted LLM agent sessions). ADK inject'ит историю корректно для OpenAI-совместимых провайдеров. Раньше не работало — исправлено в upstream `32e33a8 fix: support hosted MCP toolsets` / `d774c78 fix: use hosted llmagent sessions`. ## Проблема: MCP не работают у Валеры (provider: type=openai) ### Коренная причина hostedagent (`pkg/runtime/hostedagent/`) не поддерживает MCP инструменты. `openAIConstructor` в `agentfactory.go` получает `resolvedMCP map[string]agentconfig.MCPServerConfig`, но не передаёт их в `hostedagent.Config` — у Config нет поля для MCP. `openAIConstructor` вызывает `newHostedAgent(hostedagent.Config{...})` без MCPServers. Фикс: в `hostedagent.Config` добавить `MCPServers map[string]agentconfig.MCPServerConfig`, а в `openAIConstructor` передавать `toRuntimeMCPServers(resolvedMCP)`. Важно: hostedagent использует OpenAI-compatible API (не ACP). MCP инструменты нужно интегрировать через OpenAI tool_calls — модель шлёт tool_call, код выполняет MCP вызов. ## Проблема: MCP не работают у Валеры — попытка фикса ### Что было сделано (2026-06-19) #### 1. go.mod Добавлен `replace github.com/normahq/norma => ../norma-local` — для локальной разработки. #### 2. hostedagent — добавлена поддержка MCP - `pkg/runtime/hostedagent/agent.go`: - В Config добавлено `MCPServers map[string]acpagent.MCPServerConfig` - Сохранено в `runtimeAgent` - В `run()`: после создания модели вызывается `model.SetTools(openAIToolsFromMCPServers(r.MCPServers))` - `pkg/runtime/hostedagent/openai.go`: - В `openAIChatRequest` добавлено `Tools []openAIToolDefinition` - В `OpenAIModel` добавлены `tools []openAIToolDefinition + `SetTools()` - Добавлен `tool_calls` в парсинг ответа (`tool_calls` → content string) #### 3. agentfactory — передача MCP - В `openAIConstructor` добавлено `MCPServers: toRuntimeMCPServers(resolvedMCP)` в вызов `newHostedAgent()` #### 4. Dockerfile В `Dockerfile.claudio` добавлен `COPY norma-local/ ./norma-local/` — чтобы `replace` работал в контейнере. #### 5. Сборка Образ собран и запущен как `balda-agent-valera:latest`. #### 6. Диагностика - `mcpServerIDs` приходят корректно: `[balda obsidian fast-rlm]` - Добавлен stderr-лог в `agentfactory.go` в `Build()`: ```go fmt.Fprintf(os.Stderr, "BALDA-DEBUG: mcpServerIDs=%v resolvedMCP=%v\n", mcpServerIDs, resolvedMCP) ``` - Для просмотра логов нужен `docker logs balda-agent-valera-1 2>&1 | grep BALDA-DEBUG` ### Почему не заработало Точная причина не установлена — нужно увидеть `resolvedMCP` в логах контейнера. Возможные варианты: - `resolveMCPServers` возвращает пустой map - `toRuntimeMCPServers` неправильно конвертирует - `SetTools` не влияет на уже созданные сообщения в сессии ### Что осталось 1. Прочитать stderr из контейнера: `docker logs balda-agent-valera-1 2>&1 | grep BALDA-DEBUG` 2. Если `resolvedMCP` пустой — исправлять цепочку resolveMCPServers 3. Если не пустой — тестировать response с tool_calls в DeepSeek ответе ## Диагностика молчания ### 1. MCP ошибка: `failed to list MCP tools: failed to connect: Not Found` **Причина**: в config.yaml url без `/sse` на конце. **Фикс**: `url: http://obsidian-mcp:3101/sse` → `docker-compose restart`. ### 2. В логах `command running → command handled` за секунду, без `received provider event` **Причина**: стухший embedded NATS. Swarm внутри процесса не доставляет команду до session/task actor'ов. **Фикс**: `docker-compose restart` (если не помогло → `up -d`). ### 3. Удалён / пустой state.db После удаления balda не может зарегистрировать swarm акторы. **Фикс**: `rm state.db` и `up -d` (balda создаст заново). ### 4. Нет owner `handleMessage` выходит при `getOwnerID() == 0`. **Фикс**: прописать `allowed_owners` в config.yaml (не нужен `/start owner=`). ### 5. stream_only_with_session Сообщение в теме без сессии молча дропается. **Фикс**: написать `@Валера <текст>` чтобы создать сессию. ### 6. Проверить webhook со стороны ```bash curl -X POST http://localhost:8091/zulip/webhook \ -H "Content-Type: application/json" \ -d '{"type":"test"}' ``` ## Ключевые грабли ### OPENAI_BASE_URL — обязательная env var Norma не читает `base_url` из config.yaml для `provider: deepseek`. Читает env `OPENAI_BASE_URL`. Без неё запросы уходят на `api.openai.com` (401). **В `.env` обязательно:** ``` OPENAI_BASE_URL=https://api.deepseek.com/v1 ``` ### DOCKER_OPTS — пустая строка убивает старт Должен быть валидный JSON: ``` DOCKER_OPTS={"max_tokens": 2048, "temperature": 0.7} ``` ### composerestart vs compose up -d - `restart` — не перечитывает `.env` - `up -d` — пересоздаёт контейнер с обновлённым `.env` --- ## Диагноз: MCP не работают — инструменты не передаются модели **Статус (2026-06-21):** MCP резолвятся, но не вызываются. **Установлено через BALDA-DEBUG:** ``` BALDA-DEBUG: mcpServerIDs=[balda obsidian fast-rlm] resolvedMCP=map[balda:{http http://127.0.0.1:33545/mcp} fast-rlm:{http http://fast-rlm-mcp:3333/mcp} obsidian:{sse http://obsidian-mcp:3101/sse}] ``` MCP серверы зарезолвлены, тулсеты переданы в `hostedagent.Config`. **Почему не работают:** `OpenAIModel.generate()` → `buildChatRequest()` не читает `req.Config.Tools` (поле `genai.GenerateContentConfig`). ADK кладёт инструменты туда, но `buildChatRequest` сериализует только `model`, `messages`, `temperature`, `top_p`, `max_tokens`, `stop`. DeepSeek получает запрос без `tools` → отвечает текстом. **Логи подтверждают:** `function_call_part_count=0` в ответе DeepSeek. **Фикс:** добавить передачу tools в OpenAI запрос и парсинг tool_calls из ответа. **Сделано (2026-06-21):** в `openai.go` добавлены: - `openAIToolDefinition`, `openAIFunction`, `openAIToolCall` — структуры для API - Поле `Tools` в `openAIChatRequest` - Функция `openAIToolsFromConfig()` — конвертирует `genai.Tool[]` → OpenAI tool definitions - Функция `contentToOpenAI()` — обрабатывает FunctionCall/FunctionResponse из ADK - Парсинг `tool_calls` в `parseChatResponse()` - **Тесты:** 17 тестов на new функциональность + 3 существующих = все проходят 1. Добавить поле `Tools` в `openAIChatRequest` + `openAIToolDefinition` 2. В `buildChatRequest` читать `req.Config.Tools` → конвертировать в OpenAI tool definitions 3. В `parseChatResponse` парсить `tool_calls` из ответа (ADK сам выполнит MCP) **Важно:** upstream `norma` v0.0.10 (`bf2b25d`) уже корректно передаёт MCP тулсеты через `agentfactory.hostedToolsets()`. Проблема только в том, что `OpenAIModel` — кастомная реализация, не умеющая прокидывать tools. Если бы использовался Gemini/ACP provider — MCP бы работали.