diff --git a/personal/plans/balda-mm-phase4-agents.md b/personal/plans/balda-mm-phase4-agents.md index 1d9d4b49..ea4e9e4d 100755 --- a/personal/plans/balda-mm-phase4-agents.md +++ b/personal/plans/balda-mm-phase4-agents.md @@ -21,9 +21,10 @@ | 6c | Structured-output память на `deepseek-chat` | ⚠️ open: `dead_letter / model_failure` — память не наполняется, ответы работают. См. §13 | | 6d | Живая приёмка: Балда отвечает | ✅ **подтверждено 2026-09-26** через `mmctl post list` — бот отвечает, `duplicate tool` ушёл. См. §14 | | 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` на `0.1.12` — второй, более глубокий баг | 🟡 **корень локализован 2026-09-27** — форк включил второй раунд ADK (`TurnComplete = !hasToolCalls`) и **обнажил** двойную паковку `balda.control.shutdown` (`Tools` + `Toolset`). На `0.1.11` не проявлялся, т.к. инструменты не пакались вообще. 8 версий исключены фактами. См. §16 | -| 6g | A/B-эксперимент откатом на `0.1.11` | ⏸ **не доведён** — `0.1.11` поднят и стартовал, пост-сравнение не отправлено. Следующее действие. См. §16.4 | -| 7 | Приёмка от аккаунта Alex | 🟡 **метод готов** — `mmctl --local token generate admin` даёт токен владельца без пароля; пост от `admin` проходит авторизацию (`settlement_stage: accepted`). Блокер — ход падает на `duplicate tool` (п. 6f). См. §17 | +| 6f | 🔴 `duplicate tool` — **КОРЕНЬ НАЙДЕН И ДОКАЗАН** (2026-09-27) | ✅ **корень: двойная паковка в ADK `v2.4.0`, к форку отношения НЕ имеет** — `toolProcessor` расширяет инструменты toolset'ов в `f.Tools`, затем `toolPreprocess(f.Tools)` и `toolsetPreprocess(Toolsets)` пакуют их ДВАЖДЫ → `packTool` падает. См. §16.2 (исправлен) и §18 | +| 6g | A/B-эксперимент откатом на `0.1.11` | ✅ **доведён 2026-09-27** — `0.1.11` падает С ТЕМ ЖЕ `duplicate tool`. Гипотеза §16.2 («форк обнажил») **ОПРОВЕРГНУТА**: баг был всегда, независимо от раунда. См. §16.4 | +| 6h | Фикс двойной паковки | 🔴 **НЕ начат** — решение принято (Option A, §18.5), правка в `runtime-local/agentfactory/agentfactory.go`, ждёт go-ahead Alex. Целевой образ `balda-mm:0.1.13` | +| 7 | Приёмка от аккаунта Alex | 🟡 **метод готов и проверен** — `mmctl --local token generate admin` даёт токен владельца без пароля; пост от `admin` проходит авторизацию (`settlement_stage: accepted`, лог подтверждает). Блокер — ход падает на `duplicate tool` (п. 6f/6h). См. §17 | --- @@ -807,7 +808,7 @@ strings /tmp/chkx/usr/local/bin/balda | grep -E "tool_call_id|missing content an ⚠️ `tar -tf image.tar | grep ca-certificates` даёт **ложный 0** на OCI-образе — файлы внутри слоёв. Так же и с поиском кода форка: смотреть только распакованный бинарник. -### 16.2 🔴 Форк не «сломал» Балду — он ОБНАЖИЛ скрытый баг +### 16.2 🔴 Корень: двойная паковка инструментов в ADK `v2.4.0` (исправлено 2026-09-27) **Симптом после деплоя `0.1.12`:** каждый ход падает одинаково, Балда молчит. @@ -820,25 +821,61 @@ warn … publish event "balda.v1.evt.command.in_progress": context deadline exc **Это тот же `duplicate tool`, что чинили в §13.** В конфиге дубля НЕТ — проверено: `balda.mcp_servers: []`, ID только у провайдера. Значит причина другая. -**Механизм (по коду ADK `v2.4.0`):** -``` -base_flow.go:778 toolPreprocess() — пакует agent.Tools → req.Tools -base_flow.go:798 toolsetPreprocess() — пакует Toolsets → req.Tools - └─ mcptoolset/tool.go:89 → PackTool() → toolutils.go:47-48 - if _, ok := req.Tools[name]; ok { return fmt.Errorf("duplicate tool: %q", name) } -``` -`tools_processor.go:31` — `if f.Tools != nil { return }` — защищает **только** сборку `f.Tools`, -но **не** повторный вызов `toolsetPreprocess`. Если инструмент присутствует и в `Tools`, -и в одном из `Toolsets` (либо в двух toolsets) — второй проход падает. +> ### ⚠️ ИСПРАВЛЕНИЕ РАННЕЙ ВЕРСИИ ЭТОГО РАЗДЕЛА (2026-09-27) +> Ранее здесь было записано: «форк включил второй раунд ADK (`TurnComplete = !hasToolCalls`) и +> **обнажил** двойную паковку; на `0.1.11` её не было, т.к. инструменты не пакались вообще». +> **Это ОПРОВЕРГНУТО экспериментом §16.4.** `0.1.11` падает с ТЕМ ЖЕ `duplicate tool` при явном +> запросе инструмента. Значит: +> - паковка инструментов идёт **независимо** от `TurnComplete` — она происходит на фазе +> сборки запроса, **до** первого ответа модели; +> - второй раунд форка **не является** условием бага; +> - баг жил в связке Balda↔ADK **всегда**, на обеих версиях. +> Не воспроизводить старую формулировку: она ведёт к ложному выводу «откат на `0.1.11` лечит». -**Почему на `0.1.11` этого не было:** апстримный `openai.go` жёстко ставил `TurnComplete: true` -(:107). Агент завершал ход после **первого** ответа модели — **инструменты не пакались вообще**. -Ошибки не было, потому что не было второго раунда. Но и инструменты не работали — что и было -исходной проблемой (§14, «серверы перечислены, инструменты не исполняются»). +**Механизм — три шага, все по коду ADK `v2.4.0` (прочитано построчно):** -> **Вывод:** на `0.1.11` Балда отвечала, но инструменты были мертвы. Форк включил второй раунд -> (`TurnComplete = !hasToolCalls`) → паковка началась → **вскрылся баг сборки инструментов, -> который жил в связке Balda↔ADK всё время.** Форк не регрессия — форк диагност. +**Шаг 1.** `internal/llminternal/tools_processor.go:40-58` — `toolProcessor` **расширяет** +инструменты каждого toolset'а в `f.Tools`: +```go +if f.Tools != nil { return } // защита только от повторного расширения +tools := Reveal(llmAgent).Tools +for _, toolSet := range Reveal(llmAgent).Toolsets { + tsTools, err := toolSet.Tools(icontext.NewReadonlyContext(ctx)) + tools = append(tools, tsTools...) // 🔴 mcpTool'ы скопированы в f.Tools +} +f.Tools = tools +``` + +**Шаг 2.** `internal/llminternal/base_flow.go:765-768` — **оба** прохода подряд над ОДНИМ `req`, +между ними **нет** защиты: +```go +if err := toolPreprocess(ctx, req, f.Tools); err != nil { … } // :778 +if err := toolsetPreprocess(ctx, req); err != nil { … } // :798 +``` + +**Шаг 3.** `tool/mcptoolset/tool.go:88-90` — `mcpTool.ProcessRequest` → `toolutils.PackTool`: +```go +// toolutils/toolutils.go:47-48 +if _, ok := req.Tools[name]; ok { + return fmt.Errorf("duplicate tool: %q", name) +} +``` + +**Итог:** `f.Tools` уже содержит `balda.control.shutdown` (расширен на шаге 1) → `toolPreprocess` +кладёт его в `req.Tools` → `toolsetPreprocess` пытается положить его **второй раз** через +MCP-toolset → падение. + +🔴 **Это дефект архитектуры ADK, а не Балды и не форка.** Два слоя (tools и toolsets) трактуются +как взаимоисключающие (`if f.Tools != nil { return }`), но `base_flow` вызывает их **безусловно +оба**. `PackTool` идемпотентным не является by design. + +📌 **Почему §16.4-кандидат «`Tools` + `Toolsets` в `llmagent.Config`» — тоже не источник:** +`agentfactory.go:644` строит `toolsets = hostedToolsets(req.Toolsets, resolvedMCP)`, которая +**копирует** `req.Toolsets` и добавляет по одному toolset'у на MCP-сервер. Балда +(`internal/apps/balda/agent/builder.go:296-303`) задаёт **только `MCPServerIDs`** — `Tools` и +`Toolsets` в `BuildRequest` **пустые**. Значит MCP-серверы приходят **исключительно** как +toolsets, и «дубль в двух полях» невозможен. Источник дубля — **`toolProcessor`**, не +`agentfactory`. ### 16.3 Что исключено фактами (не повторять) @@ -857,22 +894,56 @@ base_flow.go:798 toolsetPreprocess() — пакует Toolsets → req.To вхождений подстроки**, а не число серверов: `balda` встречается в каждом логе, в `balda.v1.*`, в `balda.control.shutdown`. Не делать из этого вывод о дублях — считать **уникальные** ID. -### 16.4 Где встали и что осталось +### 16.4 ✅ A/B-эксперимент на `0.1.11` — ДОВЕДЁН, гипотеза опровергнута (2026-09-27) -**Незавершённый эксперимент (следующее действие).** Оставлен откат на `0.1.11` для A/B: -если там `duplicate tool` повторяется при **явном** вызове инструмента → баг был всегда; -если нет → он вносится вторым раундом. На момент записи: `0.1.11` поднят и стартовал -(`Balda bot started`), пост-сравнение не отправлен. +**Что сделано.** `0.1.11` уже стоял живым (`docker ps` → `balda-mm balda-mm:0.1.11 Up`, +compose строка 3 = `0.1.11`). Взял токен владельца без пароля и послал пост с **явным** +требованием вызвать инструмент: -**Рекомендованный порядок:** -1. Довести A/B на `0.1.11` (пост от владельца, см. §17 — как постить). -2. Найти, что даёт **вторую регистрацию** `balda.control.shutdown`: кандидаты — - bundled-`balda` попадает и как `Tools`, и как `Toolset` одновременно - (`agentfactory.go:655-656`: `Tools: req.Tools` + `Toolsets: toolsets`, оба в - `llmagent.Config`); либо `balda` присутствует в двух toolsets. -3. Оценить фикс **в апстриме ADK/`agentfactory`**, а не в Балде: `PackTool` идемпотентным не - является by design, значит дублировать источник нельзя. -4. Вернуть `0.1.12` после фикса. +```bash +ssh kraken 'docker exec mattermost mmctl --local token generate admin "ab-011-$(date +%H%M)"' +# → 86qus47tbfys9xehf4x5yxzuyr: ab-011-0129 + +ssh kraken '/tmp/mm-post-admin.sh 86qus47tbfys9xehf4x5yxzuyr 7bjw4g7o1jdsbmjbqd649xxgaw \ + "@balda вызови инструмент executor_health и покажи результат"' +# → posted id=hg6f1tgat7n53m345smertii5h user=cbwsorzwoigbtrdnteugnx4aqo +``` + +**Результат: `0.1.11` падает ТЕМ ЖЕ error'ом.** Лог `docker logs balda-mm --since 5m`: + +```json +{"level":"info","component":"balda.handlersfx.mattermost",…,"settlement_stage":"accepted",…} +{"level":"error","component":"balda.turn_dispatcher", + "error":"agent run: duplicate tool: \"balda.control.shutdown\"",…,"message":"turn task failed"} +{"level":"warn","component":"balda.execution_bus","delivery_attempt":1,…,"settlement_class":"retrying:unclassified"} +``` + +📌 **Авторизация прошла корректно** (`settlement_stage: accepted`, `post_user_id` = Alex) — +новый метод постинга (§17.1) работает, пост **не** отбит. До падения доходит именно ход агента. + +**Выводы (durable):** + +| Утверждение | Статус | +|---|---| +| `duplicate tool` вносится вторым раундом форка | ❌ **опровергнуто** — `0.1.11` падает так же | +| На `0.1.11` инструменты «не пакались вообще» | ❌ **опровергнуто** — паковка идёт до первого ответа модели | +| Откат на `0.1.11` — рабочее состояние | ❌ **ложно** — на `0.1.11` инструменты вызывать НЕЛЬЗЯ, ход падает | +| Баг существует независимо от версии образа | ✅ **подтверждено** | + +> 🔴 **Следствие для стратегии:** держать `0.1.11` «для отката» как рабочий вариант **нельзя** — +> он падает на любом явном вызове инструмента. Это не безопасная точка отката, а сломанное +> состояние. Единственный путь — фикс двойной паковки (§18) и `0.1.13`. + +**Метод воспроизведения (1 команда, без Alex и без GUI):** +```bash +TOK=$(ssh kraken 'docker exec mattermost mmctl --local token generate admin "ab-$(date +%s)"' | awk '{print $1}') +ssh kraken "/tmp/mm-post-admin.sh $TOK 7bjw4g7o1jdsbmjbqd649xxgaw '@balda вызови инструмент executor_health'" +sleep 45 +ssh kraken 'docker logs balda-mm --since 3m 2>&1 | grep -E "duplicate tool|turn task failed" | tail -3' +``` +📌 Пост обязан содержать **явное требование вызвать инструмент** — на обычное «скажи одно слово» +Балда отвечает текстом, инструменты не пакуются, и баг **не проявляется** (что и породило +ложный вывод «на `0.1.11` всё хорошо»). ⚠️ **Побочный эффект откатов:** жёсткое убийство контейнера оставляет незакрытый `state.db-wal`, и следующий старт на `0.1.11` идёт ~4.5 мин вместо ~1m11s (переигрывание WAL). Не считать это @@ -961,6 +1032,63 @@ ORDER BY m.date_sent DESC LIMIT 10;" --- +## 18. 🔴 Фикс двойной паковки — план (2026-09-27, ждёт go-ahead Alex) + +**Контекст:** корень доказан (§16.2), A/B проведён (§16.4). Фикс **не начат** — правка кода +в форке требует подтверждения Alex (правило: план → апрув → работа). + +### 18.1 Где чинить и почему НЕ в Балде + +| Вариант | Вердикт | +|---|---| +| Патч ADK `base_flow.go` | ❌ **невозможно без вендоринга** — `google.golang.org/adk/v2 v2.4.0` это модульная зависимость (`go.mod:27`), файлы в `$GOMODCACHE`, read-only | +| Патч `internal/apps/balda/*` | ❌ Балда задаёт только `MCPServerIDs` (`builder.go:296-303`), `Tools`/`Toolsets` пустые — дублировать нечего, чинить нечего | +| **Патч форка `runtime-local/agentfactory/agentfactory.go`** | ✅ **единственная доступная точка** — форк уже подключён через `replace` в `go.mod`, мы им владеем | + +### 18.2 Суть фикса (Option A) + +`toolProcessor` (§16.2 шаг 1) **сам** расширяет toolsets в `f.Tools` и при этом ставит +`f.Tools != nil` — после чего защита `if f.Tools != nil { return }` срабатывает и +повторное расширение не происходит. Но `toolsetPreprocess` вызывается **безусловно** и +пакует те же инструменты второй раз. + +**Решение:** не отдавать MCP-toolsets одновременно и как `Toolsets`. Для +`openAIConstructor` / `aistudioConstructor`: + +- `Toolsets:` передавать только `req.Toolsets` (без `hostedToolsets(...)`), +- MCP-инструменты добавлять в `Tools` — тогда `f.Tools != nil` и `toolsetPreprocess` + останется без MCP-toolsets, которые всё равно уже расширены в `Tools`. + +**Файл:** `runtime-local/agentfactory/agentfactory.go` +**Функции:** `openAIConstructor` (:635-658), `aistudioConstructor` (:660-685) — **обе** +(симметричный дефект), плюс `hostedToolsets` (:360-384) как источник. + +### 18.3 Порядок работ (после апрува) + +1. Правка `agentfactory.go` + тесты в `agentfactory/agentfactory_test.go`. +2. Гейты: `go build ./...`, `go test ./...`, `go tool golangci-lint run ./...` (по `AGENTS.md` форка). +3. **Тест на регрессию обязателен:** тест, который падает на текущем коде с + `duplicate tool` и проходит после фикса. +4. Сборка образа на Mac: `docker buildx build --builder arm64builder --platform linux/arm64 + -f Dockerfile.release --provenance=false --load -t balda-mm:0.1.13 .` +5. `docker save` → `scp` → `docker load` на Kraken → `tools/mm-switch-image.sh` → + `docker compose up -d --force-recreate`. +6. Приёмка (§16.4 метод): пост от владельца **с явным вызовом инструмента** → + ждём `executor_health` в ответе и **отсутствие** `duplicate tool` в логе. +7. Обновить §0 (6h → ✅) и Фазу 3 в [[personal/projects/balda/balda-kraken-mm-deploy]]. + +### 18.4 🔴 `0.1.11` — НЕ точка отката + +По итогам A/B (§16.4): на `0.1.11` любой ход с явным вызовом инструмента падает с тем же +`duplicate tool`. Держать его как «безопасный откат» **нельзя**. Образы для отката: +`0.1.10` (отвечает текстом, инструменты мертвы) — единственный, где бот **отвечает**. + +### 18.5 Открытый вопрос Alex + +Правку `runtime-local/agentfactory/agentfactory.go` (Option A) писать сейчас? + +--- + ## Связанные заметки - [[personal/projects/balda/balda-mm-delegation-plan]] — общий план (Фазы 4-5) - [[personal/projects/balda/balda-kraken-mm-deploy]] — Фаза 3 (✅) diff --git a/personal/projects/balda/balda-kraken-mm-deploy.md b/personal/projects/balda/balda-kraken-mm-deploy.md index 02e11f6d..cf43ec60 100755 --- a/personal/projects/balda/balda-kraken-mm-deploy.md +++ b/personal/projects/balda/balda-kraken-mm-deploy.md @@ -3,15 +3,28 @@ > Создан 2026-09-26. Обновлён 2026-09-26 (**✅ ФАЗА ЗАКРЫТА — боевая приёмка пройдена**). > Статус: **✅ РАБОТАЕТ. Бот отвечает аккаунту Alex.** Образ `0.1.10` (коммит `9f1e6c9c`, > он же PR baldaworks/balda#8). -> ⚠️ **АКТУАЛЬНЫЙ РАБОЧИЙ ОБРАЗ НА KRAKEN — `balda-mm:0.1.11`** (2026-09-27). -> Собран был **`0.1.12`** (форк runtime с tool-calling для `type: openai`: `openAIToolsFromConfig`, +> 🔴 **ТЕКУЩЕЕ СОСТОЯНИЕ (2026-09-27): бот отвечает ТЕКСТОМ, инструменты вызвать НЕЛЬЗЯ.** +> На Kraken стоит `balda-mm:0.1.11`. Любой ход с **явным вызовом инструмента** падает с +> `duplicate tool: "balda.control.shutdown"` — **на `0.1.11` тоже**, не только на `0.1.12` +> (A/B проведён 2026-09-27, лог подтверждён). Обычные текстовые ответы работают — отсюда +> ложное впечатление «рабочего образа». +> 🔴 **Корень (доказан по коду ADK `v2.4.0`):** двойная паковка инструментов — +> `toolProcessor` (`tools_processor.go:40-58`) расширяет инструменты toolset'ов в `f.Tools`, +> затем `base_flow.go` вызывает `toolPreprocess(f.Tools)` **и** `toolsetPreprocess(Toolsets)` +> без защиты между ними → `PackTool` (`toolutils.go:47-48`) видит имя второй раз → падение. +> **Это дефект ADK, не Балды и не форка.** Полный разбор — [[personal/plans/balda-mm-phase4-agents]] §16.2. +> Фикс (Option A, патч форка `runtime-local/agentfactory/agentfactory.go`) спланирован, но +> **не начат** — ждёт go-ahead Alex. Целевой образ `balda-mm:0.1.13`. См. §18 того же дока. +> ⚠️ Собран был **`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**; > в Балде — `replace github.com/normahq/runtime/v2 => ./runtime-local`). -> 🔴 **Но `0.1.12` откачен на `0.1.11`:** форк включил второй раунд ADK и **обнажил** двойную -> паковку инструментов → `duplicate tool: "balda.control.shutdown"` на каждом ходу, Балда молчала. -> На `0.1.11` Балда отвечает, но инструменты не исполняются (инструменты не пакались вообще). -> Разбор и следующий шаг — [[personal/plans/balda-mm-phase4-agents]] §16. +> 🔴 **`0.1.12` откачен на `0.1.11` — но это оказалось НЕ лечением** (2026-09-27): ранняя +> версия этого дока утверждала, что форк «обнажил» двойную паковку. **Опровергнуто A/B:** +> `0.1.11` падает тем же `duplicate tool` при явном вызове инструмента. Паковка инструментов +> идёт на сборке запроса, **до** первого ответа модели, и от второго раунда 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`). > **Доказательство (лента `balda-test`):** `cbwsorzwoigbtrdnteugnx4aqo` (Alex) — > `@balda скажи одно слово` → `edxrgupoxiyig5gzmr79tjypbme` (бот) — `слово`.