9.4 KiB
Executable File
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):
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:
// 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) — тот же эндпоинт +?<contextQueryParameter>=<token>Wrap()(:118-153) — тот же handler, инжектит trusted-заголовки по токену
agent/runtime_manager.go:344-345 добавляет привязку в набор серверов агента:
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-<token>"
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 toolset». Искать строго
grep -c "duplicate tool:" (с двоеточием, как в формате PackTool).
⚠️ Тест должен быть чувствительным. Первая версия проверяла только количество тулсетов и
проходила даже со сломанным фиксом (отключение keeper-предпочтения через if false && … её
не ломало). Добавлен selectScopedOnly — теперь тест падает, если выживает не scoped-запись.
Слабый тест = ложная уверенность.
6. Метод: исполняемый пробник вместо чтения
Час чтения исходников ADK дал неверную версию. Один тест на присутствие интерфейса убил её за секунду:
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)