diff --git a/personal/plans/balda-mm-phase4-agents.md b/personal/plans/balda-mm-phase4-agents.md index 4af5dfa7..2fe17311 100755 --- a/personal/plans/balda-mm-phase4-agents.md +++ b/personal/plans/balda-mm-phase4-agents.md @@ -23,8 +23,8 @@ | 6e | 🔴 MCP-инструменты НЕ исполняются | ✅ **патч готов 2026-09-27** — форк `mallexxx/runtime` + PR [#4](https://github.com/normahq/runtime/pull/4), `replace` в `go.mod`, образ `balda-mm:0.1.12`. См. §15-16 | | 6f | 🔴 `duplicate tool` — **КОРЕНЬ НАЙДЕН И ДОКАЗАН** (2026-09-27) | ✅ **корень: Balda регистрирует bundled-сервер ДВАЖДЫ на одном URL** (`balda` + `balda-session-memory-`), `hostedToolsets` строит 2 тулсета с одинаковыми инструментами → `PackTool` падает. ⚠️ Прежняя версия («двойная паковка в ADK через `toolPreprocess`+`toolsetPreprocess`») **ОПРОВЕРГНУТА исполняемым тестом** — `mcptoolset.set` не реализует `ProcessRequest`. См. §19 | | 6g | A/B-эксперимент откатом на `0.1.11` | ✅ **доведён 2026-09-27** — `0.1.11` падает С ТЕМ ЖЕ `duplicate tool`. Гипотеза §16.2 («форк обнажил») **ОПРОВЕРГНУТА**: баг был всегда, независимо от раунда. См. §16.4 | -| 6h | Фикс дублирующегося тулсета | 🟡 **КОД НАПИСАН 2026-09-27** — дедуп по эндпоинту в `hostedToolsets` (`runtime-local/agentfactory/agentfactory.go`) + 4 теста в `duplicate_toolset_test.go`. `go build`/`go vet` ✅, тесты **не прогнаны**. Остаётся: тесты → образ `balda-mm:0.1.13` → деплой → приёмка. См. §19.3-19.5 | -| 7 | Приёмка от аккаунта Alex | 🟡 **метод готов и проверен** — `mmctl --local token generate admin` даёт токен владельца без пароля; пост от `admin` проходит авторизацию (`settlement_stage: accepted`, лог подтверждает). Блокер — ход падает на `duplicate tool` (п. 6f/6h). См. §17 | +| 6h | Фикс дублирующегося тулсета | ✅ **ИСПРАВЛЕНО 2026-09-27, `balda-mm:0.1.13` задеплоен** — дедуп по эндпоинту в `hostedToolsets` (`runtime-local/agentfactory/agentfactory.go`) + 6 тестов. `go build`/`go vet`/`go test ./...` ✅. Приёмка от владельца: `@balda вызови executor_health` → ответ `**executor_health:** ok`, `turn_complete: true`, `duplicate tool:` = **0**, deadletter = **0**. См. §19.3-19.5 | +| 7 | Приёмка от аккаунта Alex | ✅ **ПРОЙДЕНА 2026-09-27** — пост владельца `cbwsorzwoigbtrdnteugnx4aqo` → ответ бота `edxrgupoxiyig5gzmr79tjypbme`: `**executor_health:** ok`. `settlement_stage: accepted`, `turn_complete: true`, инструмент исполнился. См. §17, §19.5 | --- @@ -1205,7 +1205,37 @@ for _, id := range ids { | `TestMCPEndpointKey` | `?ctx=tok`, `?a=1&b=2` → ключ без query | | `TestMcpToolsetImplementsRequestProcessor` | фиксирует факт: `mcptoolset.set` **не** имеет `ProcessRequest` | -### 19.5 Состояние и остаток работ +### 19.5 ✅ ПРИЁМКА ПРОЙДЕНА — `balda-mm:0.1.13` (2026-09-27) + +Гейты: `go build ./...` ✅ · `go vet ./...` ✅ · `go test ./...` форка ✅ · +`go test ./agentfactory/ -run 'Toolset|MCPEndpoint|McpToolset' -v` → **6/6 PASS**. + +Деплой: compose `image: balda-mm:0.1.13` (бэкап `docker-compose.yml.bak-013-`), +`docker compose up -d --force-recreate`, старт OK (`Balda bot started`). + +**Приёмка от владельца** (`@balda вызови executor_health и покажи результат`): + +``` +1790454574509 | cbwsorzwoigb | @balda вызови executor_health и покажи результат +1790454694404 | edxrgupoxiyg | **executor_health:** `ok` +``` + +Метрики нового контейнера: + +| Проверка | Значение | +|---|---| +| `duplicate tool:` (ошибка) | **0** ✅ | +| `turn task failed` / `command deadlettered` | **0** ✅ | +| `turn_complete` / `final_response` | `true` / `true` ✅ | +| `function_response_part_count` | **1** — инструмент реально вызван ✅ | +| `delivery settled` (`agent_reply`) | `success` ✅ | +| Строка фикса в логе | `[mcp] skipping duplicate toolset "balda": endpoint already served by "balda-session-memory-"` ✅ | + +⚠️ **Питфолл своей же проверки:** `grep -c "duplicate tool"` даёт **1**, потому что моя строка +лога — «skipping **duplicate tool**set». Искать ошибку надо строго как `grep -c "duplicate tool:"` +(с двоеточием, как в формате `PackTool`). Иначе ложная тревога на ровном месте. + +Остаток: §0 строка 6e (форк) — закрыта ранее; **6f/6g/6h/7 — ✅**. Гейты (прогнано 2026-09-27): `go build ./...` → **OK**, `go vet ./...` → **OK**, `go test ./...` в форке → **все зелёные**. Целевой прогон: @@ -1213,6 +1243,21 @@ for _, id := range ids { видно требуемое поведение: `[mcp] skipping duplicate toolset "balda": endpoint already served by "balda-session-memory-abc123"` — выживает именно scoped-запись. +### 19.6 Остаток работ (незначительный) + +| # | Пункт | Почему не срочно | +|---|---|---| +| 1 | Удалить с Kraken диагностический образ `balda-mm:0.1.13-diag` | Он уже не используется: compose указывает на `0.1.13`. Занимает ~79 МБ | +| 2 | Почистить старые теги `balda-mm:0.1.0`…`0.1.9` | Оставлены ранее осознанно для отката; `0.1.10`-`0.1.13` трогать не нужно | +| 3 | Завести Access Application на `agents-bridge.qentra.top` | Отдельный пункт §9.5, к этому дефекту не относится | + +⛔ **Не удалять** без явного подтверждения Alex (правило: никаких удалений без апрува). + +⚠️ **Урок про перегенерацию образа:** сборка, запущенная **до** последней правки кода, даёт +устаревший бинарник. В этой сессии так и случилось: build стартовал, затем правки в +`mcpEndpointKey` (добавлен `sse`), пришлось убивать процесс и пересобирать. Порядок: +**сначала все правки → финальный `go build`/`go test` → и только потом docker build.** + 🔴 **Проверка чувствительности теста:** отключение keeper-предпочтения (`if false && …`) тест **не ломает** — он проверял только количество. Поэтому добавлен `selectScopedOnly`: теперь тест падает, если выживает не scoped-запись. Слабый тест = ложная уверенность. diff --git a/personal/projects/balda/balda-kraken-mm-deploy.md b/personal/projects/balda/balda-kraken-mm-deploy.md index b33152d9..19278252 100755 --- a/personal/projects/balda/balda-kraken-mm-deploy.md +++ b/personal/projects/balda/balda-kraken-mm-deploy.md @@ -1,14 +1,11 @@ # Фаза 3 — Балда на Kraken + Mattermost (деплой) -> Создан 2026-09-26. Обновлён 2026-09-26 (**✅ ФАЗА ЗАКРЫТА — боевая приёмка пройдена**). -> Статус: **✅ РАБОТАЕТ. Бот отвечает аккаунту Alex.** Образ `0.1.10` (коммит `9f1e6c9c`, -> он же PR baldaworks/balda#8). -> 🔴 **ТЕКУЩЕЕ СОСТОЯНИЕ (2026-09-27): бот отвечает ТЕКСТОМ, инструменты вызвать НЕЛЬЗЯ.** -> На Kraken стоит **диагностический** `balda-mm:0.1.13-diag` (не `0.1.11` — оно подменено в -> ходе сессии). Любой ход с **явным вызовом инструмента** падает с -> `duplicate tool: "balda.control.shutdown"` — на `0.1.11` тоже, не только на `0.1.12` -> (A/B проведён 2026-09-27, лог подтверждён). Обычные текстовые ответы работают — отсюда -> ложное впечатление «рабочего образа». +> Создан 2026-09-26. Обновлён 2026-09-27 (**✅ ФАЗА ЗАКРЫТА — боевая приёмка пройдена, включая +> ВЫЗОВ ИНСТРУМЕНТОВ**). +> Статус: **✅ РАБОТАЕТ. Бот отвечает аккаунту Alex и исполняет MCP-инструменты.** +> Текущий образ на Kraken — **`balda-mm:0.1.13`** (см. блок ниже). Первая приёмка боевого +> пути была на `0.1.10` (коммит `9f1e6c9c`, он же PR baldaworks/balda#8) — но тогда +> инструменты не исполнялись. > 🔴 **КОРЕНЬ (исправлено 2026-09-27, проверено кодом):** Balda регистрирует bundled MCP-сервер > **ДВАЖДЫ на одном URL** — `registry.Set("balda", …)` (`internalmcp/manager.go:213`) и > session-scoped привязка `balda-session-memory-` (`sessionmemorymcp/broker.go:111`, @@ -19,8 +16,12 @@ > ОПРОВЕРГНУТА исполняемым тестом:** `mcptoolset.set` **не реализует** `ProcessRequest` > (метод есть только у `mcpTool`), поэтому `toolsetPreprocess` пропускает MCP-тулсеты. > Это дефект **регистрации в Балде**, не ADK и не форка. Разбор — [[personal/plans/balda-mm-phase4-agents]] §19. -> Фикс **написан** (дедуп тулсетов по эндпоинту в `hostedToolsets`), `go build`/`go vet` ✅, -> тесты не прогнаны. Целевой образ `balda-mm:0.1.13`. См. §19.3-19.5 того же дока. +> ✅ **ФИКС ЗАДЕПЛОЕН И ПРИНЯТ (2026-09-27).** Образ **`balda-mm:0.1.13`** — дедуп тулсетов по +> эндпоинту в `hostedToolsets` (запись со scoped-URL выживает: несёт контекст-токен). +> Приёмка от владельца: `@balda вызови executor_health и покажи результат` → +> `**executor_health:** ok`. `duplicate tool:` = **0**, deadletter = **0**, +> `turn_complete`/`final_response` = true, `function_response_part_count` = 1 +> (инструмент реально исполнен). Детали — §19.5 того же дока. > ⚠️ Собран был **`0.1.12`** (форк runtime с tool-calling для `type: openai`: `openAIToolsFromConfig`, > `parseChatResponse`, `TurnComplete = !hasToolCalls`; форк `mallexxx/runtime`, ветка > `feat/openai-tool-calls-v2`, коммит `733b32d`, PR **https://github.com/normahq/runtime/pull/4**; @@ -31,7 +32,9 @@ > идёт на сборке запроса, **до** первого ответа модели, и от второго раунда ADK не зависит. > Баг жил в связке Balda↔ADK всегда. > Разбор и следующий шаг — [[personal/plans/balda-mm-phase4-agents]] §16.2 и §18. -> Compose: `/home/kraken/balda/docker-compose.yml` (`image: balda-mm:0.1.11`). +> ⛔ **§16.2 и §18 того дока — ОПРОВЕРГНУТЫ, читать только как историю.** Актуальный корень и +> фикс — §19. Не повторять «это ADK / это форк». +> Compose: `/home/kraken/balda/docker-compose.yml` (`image: balda-mm:0.1.13`). > **Доказательство (лента `balda-test`):** `cbwsorzwoigbtrdnteugnx4aqo` (Alex) — > `@balda скажи одно слово` → `edxrgupoxiyig5gzmr79tjypbme` (бот) — `слово`. > `settlement_stage=accepted`, `bad_seq`=0, `RestartCount`=0. diff --git a/personal/tech/adk-hostedagent-openai-mcp.md b/personal/tech/adk-hostedagent-openai-mcp.md new file mode 100644 index 00000000..ce8f91b8 --- /dev/null +++ b/personal/tech/adk-hostedagent-openai-mcp.md @@ -0,0 +1,181 @@ +# ADK hostedagent — OpenAI/MCP: `duplicate tool` и правда о `ProcessRequest` + +> Создано 2026-09-27. Зеркало скилла [[software-development/adk-hostedagent-openai-mcp|`adk-hostedagent-openai-mcp`]]. +> Здесь — **исправление к скиллу** и разбор дефекта, найденного на Балде. +> Проект: [[personal/plans/balda-mm-phase4-agents]] §19, [[personal/projects/balda/balda-kraken-mm-deploy]] §19. + +--- + +## 1. ⚠️ Исправление к скиллу (было неверно до 2026-09-27) + +Скилл (v3.2.0) утверждал: + +> «**MCP tools implement `RequestProcessor`** → их `ProcessRequest()` вызывает +> `toolutils.PackTool(req, tool)`, поэтому инструменты пакуются и в `toolPreprocess`, и в +> `toolsetPreprocess`». + +**Это ложь.** Проверено исполняемым тестом: + +``` +go test ./agentfactory/ -run TestMcpToolsetImplementsRequestProcessor -v +→ MCP toolset "mcp_tool_set" does NOT implement ProcessRequest +``` + +В `adk/v2@v2.4.0`: + +| Тип | Файл | `ProcessRequest` | +|---|---|---| +| `mcpTool` | `tool/mcptoolset/tool.go:88` | ✅ есть | +| `set` (сам тулсет) | `tool/mcptoolset/set.go:144-185` | ❌ **НЕТ** (есть только `Tools()`) | + +Поэтому `toolsetPreprocess` (`base_flow.go:798`) на MCP-тулсетах делает `if !ok { continue }` — +**пропускает их**. Единственный паковщик для MCP-инструментов — `toolPreprocess` (`:778`) +по плоскому `Flow.Tools`, который наполняет `toolProcessor` (`tools_processor.go:49`). + +⛔ **Не диагностировать `duplicate tool` как «двойную паковку двух слоёв ADK» — этот путь +физически невозможен.** Полчаса чтения `base_flow.go` вели именно к этой ложной версии. + +--- + +## 2. Как выглядит дефект + +``` +error … "agent run: duplicate tool: \"balda.control.shutdown\"" +warn … command failed with retryable error (delivery_attempt 1..5) +warn … command deadlettered (terminal:unclassified) +``` + +`PackTool` (`tool/toolutils/toolutils.go:47-48`): + +```go +if _, ok := req.Tools[name]; ok { + return fmt.Errorf("duplicate tool: %q", name) +} +``` + +`PackTool` **не идемпотентен**, а `toolProcessor` схлопывает **все** тулсеты в один плоский +`Flow.Tools`. Значит: **два MCP-сервера с одинаковыми именами инструментов = падение хода.** + +🔴 **Ключевая сигнатура:** обычный текстовый ответ работает, падает **только** ход с явным +запросом инструмента. Это указывает на момент паковки, а не на конфиг. + +--- + +## 3. Настоящий корень (найден 2026-09-27) + +Balda регистрировала bundled MCP-сервер **ДВАЖДЫ на один и тот же URL**: + +```go +// internal/apps/balda/internalmcp/manager.go +// :206 — база для session-scoped брокера +m.contextBroker.SetBaseURL("http://" + res.Addr + baldaRoute) +// :213 — и тот же эндпоинт в реестр +m.registry.Set(bundledBaldaServerID, agentconfig.MCPServerConfig{ + Type: agentconfig.MCPServerTypeHTTP, + URL: fmt.Sprintf("http://%s%s", res.Addr, baldaRoute), +}) +``` + +`sessionmemorymcp/broker.go`: +- `Bind()` (`:111`) → `ID: "balda-session-memory-"+token`, а URL (через `contextURL:216-225`) — + **тот же эндпоинт** + `?=` +- `Wrap()` (`:118-153`) — тот же handler, инжектит trusted-заголовки по токену + +`agent/runtime_manager.go:344-345` добавляет привязку в набор серверов агента: + +```go +registry.Set(binding.ID, binding.Config) +extraMCPServerIDs = append(extraMCPServerIDs, binding.ID) +``` + +→ `hostedToolsets` строит **два тулсета на один сервер** → одинаковые имена → `PackTool` падает. + +**Триггер:** `balda.session_memory.enabled: true` создаёт scoped-привязку. Без неё дефекта нет. + +--- + +## 4. Фикс — дедуп по эндпоинту + +**Файл:** `runtime-local/agentfactory/agentfactory.go`, функция `hostedToolsets`. + +Два конфига, адресующие **один host+path**, — один сервер с точки зрения пространства имён +инструментов. Различие в query (контекст-токен) не делает их разными. + +Хелперы: +- `mcpEndpointKey(cfg)` — `url.Parse`, обнуляет `RawQuery` и `Fragment`. + Покрывает **`http` и `sse`**; `stdio` возвращает `""` (общего эндпоинта нет) → никогда не схлопывается. +- `mcpURLIsScoped(cfg)` — есть ли query (признак session-scoped привязки Балды). + +🔴 **Критично, какую запись оставлять.** Наивный «первый по алфавиту» оставляет `balda` и +**выбрасывает scoped-привязку** — краш уходит, но **session memory ломается** (теряется +контекст-токен). Поэтому keeper — запись **со** scoped-URL: она строгий супернабор +(без токена `Wrap` просто пропускает запрос в `next.ServeHTTP`). + +Результат в логе: + +``` +[mcp] skipping duplicate toolset "balda": endpoint already served by "balda-session-memory-" +``` + +--- + +## 5. Проверка (что реально подтверждает фикс) + +| Проверка | Команда | Ожидание | +|---|---|---| +| Тесты | `go test ./agentfactory/ -run 'Toolset\|MCPEndpoint\|McpToolset' -v` | 6/6 PASS | +| Ошибка ушла | `docker logs balda-mm \| grep -c "duplicate tool:"` | **0** | +| Инструмент вызван | лог: `function_response_part_count` | **1** | +| Ход завершён | `turn_complete` / `final_response` | `true` / `true` | +| Живая приёмка | пост владельца с явным вызовом инструмента | ответ с реальным результатом | + +⚠️ **Питфолл своей же проверки:** `grep -c "duplicate tool"` даёт ложную **1**, потому что +собственная строка лога фикса — «skipping **duplicate tool**set». Искать строго +`grep -c "duplicate tool:"` (с двоеточием, как в формате `PackTool`). + +⚠️ **Тест должен быть чувствительным.** Первая версия проверяла только *количество* тулсетов и +**проходила даже со сломанным фиксом** (отключение keeper-предпочтения через `if false && …` её +не ломало). Добавлен `selectScopedOnly` — теперь тест падает, если выживает не scoped-запись. +Слабый тест = ложная уверенность. + +--- + +## 6. Метод: исполняемый пробник вместо чтения + +Час чтения исходников ADK дал **неверную** версию. Один тест на присутствие интерфейса убил её +за секунду: + +```go +type requestProcessor interface { + ProcessRequest(ctx any, req any) error +} +if _, ok := toolset.(requestProcessor); ok { + t.Fatal("unexpected: premise changed") +} +``` + +📌 **Правило:** когда гипотеза касается **интерфейса или пути вызова** — писать исполняемый +пробник, а не читать дальше. Чтение даёт правдоподобные версии; тест даёт факт. + +📌 **Искать ошибку в БАЗОВОМ модуле, а не в своём репо:** `grep -rn "duplicate tool"` по +репозиторию Balda **пуст** — текст живёт в `google.golang.org/adk` (`$GOMODCACHE`). Симптом +«в моём коде такого нет» означает «ищи в зависимостях». + +--- + +## 7. Порядок сборки образа (урок сессии) + +Сборка, запущенная **до** последней правки кода, даёт устаревший бинарник. В этой сессии build +стартовал, затем я доработал `mcpEndpointKey` (добавил `sse`), пришлось убивать процесс и +пересобирать. + +**Правильный порядок:** все правки → финальный `go build` / `go test` → и только затем docker build. + +--- + +## Связанные заметки + +- [[personal/plans/balda-mm-phase4-agents]] — §19 (корень, фикс, тесты, приёмка), §16.2 и §18 опровергнуты +- [[personal/projects/balda/balda-kraken-mm-deploy]] — Фаза 3, образ `balda-mm:0.1.13` +- [[personal/tech/balda-go-fx-pitfalls]] — fx-питфоллы Балды +- скилл `adk-hostedagent-openai-mcp` — источник (исправлен 2026-09-27)