Перейти к содержимому

Практика интеграции OpenCode: эволюция архитектуры от отдельных процессов к общему Runtime

HagiCode for Windows Microsoft Store artwork
HagiCode for Windows is now on Microsoft Store
HagiCode for Windows is officially live on Microsoft Store. Windows users can install it directly from the storefront and stay on the store-managed update path. Open the listing and take a look.
Open Microsoft Store

Практика интеграции OpenCode: эволюция архитектуры от отдельных процессов к общему Runtime

В этой статье мы делимся полным опытом интеграции AI-помощника OpenCode в HagiCode, включая ключевые проектные решения в процессе эволюции архитектуры, встреченные проблемы и конечные решения.

Предпосылки

OpenCode — это проект AI-помощника для программирования с открытым исходным кодом, размещённый на GitHub. Для такого monorepo-проекта, как HagiCode, интеграция OpenCode в качестве поддерживаемого AI Provider означает возможность использовать его как бэкенд-модель при генерации предложений, редактировании кода и выполнении рабочих процессов.

Однако процесс интеграции оказался не таким гладким, как хотелось бы. Раньше существовали два независимых предложения: одно планировало создать C# SDK, позже было отменено — на самом деле это не было большой потерей; другое занималось интеграцией на уровне репозитория и выдержало испытание временем. С выходом OpenCode в официальный канал сеансов возникла серия проблем с управлением сеансами, восстановлением после ошибок и т. д. — неизбежное всегда приходит.

Ещё более головной болью стало то, что изначально спроектированная модель “отдельный процесс на каждый сеанс” в реальной эксплуатации выявила проблему больших накладных расходов ресурсов, пришлось реструктурировать в модель “общий runtime на уровне системы”. Также наступили на грабли 400 BadRequest — повторное использование внешней конечной точки без контекста приводило к отказу запроса, об этом и говорить не стоит.

Эта статья и есть систематизация этих проблем и принятых проектных решений, чтобы дать некоторую ориентацию для проектов, которым впоследствии потребуется интегрировать OpenCode. Ведь прекрасные вещи или люди не обязательно должны принадлежать тебе, пока она остаётся прекрасной, хорошо просто смотреть на её красоту… Технический обмен тем же самым.

О HagiCode

Решения, которыми мы делимся в этой статье, основаны на нашем практическом опыте в проекте HagiCode. HagiCode — это проект AI-помощника для программирования, в процессе разработки нам нужно было интегрировать несколько AI Provider, и OpenCode — один из них. Следующий процесс эволюции архитектуры — это реальный опыт, полученный в реальном проекте через ошибки и оптимизацию, выбора нет, ошибки нужно исправлять.

Техническая архитектура

Общий слоистый дизайн

Архитектура интеграции OpenCode в HagiCode разделена на пять слоёв, каждый с чёткими обязанностями:

1. Слой интеграции репозитория

Через систему конфигурации MonoSpecs (.hagicode/monospecs.yaml) регистрируется репозиторий OpenCode. Здесь есть выбор: submodule или обычный Git-репозиторий? Мы выбрали последний, через унифицированный скрипт scripts/clone-repos.mjs управляем клонированием и синхронизацией. Это более гибко и также позволяет избежать проблем с правами доступа и сотрудничеством, связанных с submodule — ведь никто не хочет видеть那张报错的照,可是没辙.

2. Слой Provider

OpenCodeCliProvider реализует интерфейс IAIProvider, это стандартный уровень абстракции для подключения внешних AI-сервисов. Первоначальное предложение хотело сделать “отдельный процесс на каждый сеанс”, но в реальной работе обнаружили, что накладные расходы ресурсов слишком велики, в конечном итоге изменили на модель общего runtime, через OpenCodeRuntimeCoordinator управляем жизненным циклом runtime на уровне системы. Это тоже ничего, идея прекрасна, реальность жестока.

3. Слой управления Runtime

OpenCodeRuntimeCoordinator — ядро всей архитектуры, отвечает за запуск runtime, проверку работоспособности и пересоздание при сбоях. Он использует HagiCode.Libs.Providers.OpenCode как основу HTTP-клиента, инкапсулирует все взаимодействия с runtime OpenCode. Как той зимней ночью, бамбук за окном остаётся таким же, как вчера,少了那份对她的回应,она всё также любит смотреть за окно — runtime也是如此,需要有人默默守护.

4. Слой персистентности сеансов

Используется база данных SQLite (opencode-session-bindings-v2.db) для персистентности отображения CessionId в OpenCode SessionId. Этот дизайн очень важен, он поддерживает восстановление и перезапуск сеансов, избегая создания нового сеанса каждый раз. Ведь память — иногда забывание лучше, но в мире программ без памяти действительно не обойтись.

5. Слой восстановления ошибок

ProviderErrorAutoRetryCoordinator предоставляет механизм автоматического повтора, в сочетании с OpenCodeRetryableTerminalFailureClassifier классифицирует ошибки — какие можно повторить, какие должны сразу завершиться неудачей. Этот слой значительно повышает устойчивость системы. На самом деле это ничего, просто даёт системе возможность как человеку — упасть и снова подняться.

Ключевой поток данных

Когда приходит AI-запрос, поток данных выглядит так:

  1. Запрос сначала поступает в OpenCodeCliProvider
  2. Provider запрашивает runtime у OpenCodeRuntimeCoordinator
  3. Coordinator проверяет наличие доступного runtime, если нет — запускает новый
  4. Через CessionId запрашивает или создаёт привязку сеанса
  5. Использует привязанный SessionId для вызова OpenCode API
  6. При ошибке, в зависимости от типа ошибки, решает, нужно ли повторять

Этот процесс кажется простым, но на каждом этапе наступали на грабли. Есть ли в этом смысл? Возможно, в любом случае уже наступали… Также поняли, что наступание на грабли — это часть роста.

Ключевые проектные решения

От отдельных процессов к общему Runtime

Первоначальное предложение opencode-csharp-sdk использовало модель “один отдельный процесс на каждый сеанс”. Идея прекрасна: хорошая изоляция, падение одного процесса не влияет на другие сеансы. Но реальность жестока:

  • Большие накладные расходы ресурсов: каждый процесс должен загружать runtime, использование памяти линейно возрастает
  • Медленный запуск: частое создание и уничтожение процессов, накладные расходы нельзя игнорировать
  • Сложное управление: управление жизненным циклом процессов само по себе troublesome

В конечном итоге мы изменили на модель “общий runtime на уровне системы”. Все сеансы используют один и тот же процесс runtime, через session id различают разные сеансы. Это изменение снизило использование ресурсов на порядок, а скорость отклика значительно улучшилась. На самом деле это ничего, просто превратили “один человек наслаждается в одиночку” в “все вместе используют”.

Самоуправляемая конечная точка vs внешний BaseUri

Раньше возникла странная проблема 400 BadRequest. При расследовании выяснилось, что это из-за повторного использования внешнего BaseUrl без необходимой контекстной информации. Runtime OpenCode является stateful, прямое использование внешней конечной точки эквивалентно потере контекста — как человек, потерявший память, в растерянности.

Решение простое: поддерживать самоуправляемый runtime, не зависеть от внешних конечных точек. В конфигурационном файле BaseUri оставляем пустым, позволяя системе самой управлять жизненным циклом runtime.

AI:
OpenCode:
Enabled: true
ExecutablePath: "opencode"
BaseUri: null # 留空,使用自管 runtime
Model: "anthropic/claude-sonnet-4-20250514"

Это изменение конфигурации кажется незначительным, но решило тогда самую головную боль проблему. Ведь иногда ответ прямо перед глазами, только мы слишком много свернули.

Стратегия привязки сеансов

Привязка сеансов — другое ключевое решение. Мы используем CessionId как ключ привязки, поддерживаем три режима:

  • started: новый сеанс, создаётся новый OpenCode SessionId
  • resumed: возобновление существующего сеанса, привязка читается из базы данных
  • restarted: перезапуск сеанса, создаётся новый SessionId, но сохраняется история

Этот дизайн делает управление сеансами очень гибким, пользователи могут в любое время возобновить предыдущий диалог, система также может автоматически восстановить привязку после перезапуска runtime. Ведь память — иногда хочешь забыть, но не можешь, иногда хочешь запомнить, но не получается… Память в мире программ довольно надёжна.

План реализации

1. Интеграция репозитория

Зарегистрировать репозиторий OpenCode в .hagicode/monospecs.yaml:

repositories:
- path: "repos/opencode"
url: "https://github.com/anomalyco/opencode.git"
displayName: "OpenCode"
icon: "⌨️"

Затем запустить скрипт клонирования:

Terminal window
node scripts/clone-repos.mjs

Таким образом, исходный код OpenCode будет загружен локально, в дальнейшем можно обновлять в любое время. На самом деле это довольно просто, главное чтобы не было ошибок…

2. Конфигурация Provider

Настроить OpenCode provider в appsettings.yml:

AI:
OpenCode:
Enabled: true
ExecutablePath: "opencode"
BaseUri: null
Model: "anthropic/claude-sonnet-4-20250514"
RequestTimeoutSeconds: 300
StartupTimeoutSeconds: 60

Несколько ключевых параметров:

  • RequestTimeoutSeconds: время ожидания одиночного запроса, по умолчанию 5 минут — ведь слишком долгое ожидание тоже мучительно
  • StartupTimeoutSeconds: время ожидания запуска runtime, даём достаточно 1 минуту

3. Восстановление Provider

Вновь включить OpenCode в систему AI Provider:

  • В перечислении AIProviderType восстановить OpenCodeCli
  • В AIProviderFactory восстановить логику создания
  • ExecutorGrainFactory маршрутизирует OpenCodeCli в специализированный grain

Эти изменения делают OpenCode равноправным AI Provider, а не особым случаем. На самом деле все одинаковые, нет ничего особенного, не особенного.

4. Пример кода управления Runtime

// Получаем runtime через OpenCodeRuntimeCoordinator
var runtime = await _runtimeCoordinator.GetRuntimeAsync(
_settings,
request.WorkingDirectory,
cancellationToken);
// Создаём или восстанавливаем session
var session = await ResolveSessionAsync(runtime, request, cancellationToken);
// Отправляем prompt
var response = await session.Runtime.Client.PromptAsync(
session.SessionId,
promptRequest,
cancellationToken);

Этот код кажется очень простым, но за ним проделана большая работа: запуск runtime, проверка работоспособности, запрос и создание привязки сеанса. Как и многие вещи, на поверхности ничего не видно, а за всем стоит история.

5. Механизм восстановления ошибок

// Обнаруживаем повторяемые ошибки и пересоздаём runtime
if (ShouldRetryWithFreshRuntime(ex, cancellationToken))
{
await _runtimeCoordinator.InvalidateAsync(runtime, ...);
var recoveredRuntime = await ResolveRuntimeAsync(request, cancellationToken);
// Повторяем с новым runtime
}

Механизм автоматического повтора значительно повышает устойчивость системы, сетевые抖动, случайные сбои runtime могут автоматически восстанавливаться. На самом деле жизнь также如此,упал — встань, ничего страшного… Программы намного крепче людей.

Практическое руководство

Справочник по ключевой конфигурации

КонфигурацияПо умолчаниюОписание
EnabledtrueВключен ли OpenCode provider
ExecutablePath"opencode"Путь к исполняемому файлу OpenCode
BaseUrinullВнешняя конечная точка (рекомендуется оставить пустым)
Model-Модель по умолчанию
RequestTimeoutSeconds300Время ожидания запроса
StartupTimeoutSeconds60Время ожидания запуска Runtime

Структура базы данных привязки сеансов

CREATE TABLE IF NOT EXISTS OpenCodeSessionBindings (
BindingKey TEXT NOT NULL PRIMARY KEY,
OpenCodeSessionId TEXT NOT NULL,
CreatedAtUtc TEXT NOT NULL,
UpdatedAtUtc TEXT NOT NULL
);

Привязка хранится 30 дней, по истечении срока автоматически очищается. Этот дизайн гарантирует возможность восстановления сеанса и избегает бесконечного роста данных. Ведь у всего есть срок, по истечении срока очищается — это также своего рода освобождение…

Частые проблемы и решения

1. Ошибка 400 BadRequest

Проверьте конфигурацию BaseUri, рекомендуется оставить пустым для использования самоуправляемого runtime. Если необходимо использовать внешнюю конечную точку, убедитесь в полноте контекста. На самом деле в большинстве случаев проблема заключается в “очевидном”.

2. Не удаётся восстановить сеанс

Убедитесь, что CessionId правильно передаётся, проверьте, существует ли соответствующая запись привязки в базе данных. Как при поиске памяти — нужны улики.

3. Проблема выбора модели

Поддерживаются два формата: provider/model (например anthropic/claude-sonnet-4) и формат без provider (например claude-sonnet-4). Все дороги ведут в Рим, только некоторые дороги легче, некоторые немного более извилисты.

4. Несовпадение имён инструментов

Имена инструментов автоматически нормализуются, удаляется содержимое после скобок и двоеточий. Например read(path) станет read, при вызове нужно обратить внимание. Эти детали тоже не считаются чем-то, просто легко игнорируются.

5. Автоматический повтор не работает

Проверьте, правильно ли классификатор ошибок идентифицирует повторяемые ошибки. По умолчанию сетевые ошибки, сбой runtime и т. д. автоматически повторяются до 3 раз. Ведь попробовать ещё раз ничего, возможно получится.

Связанные пути к коду

  • Provider: repos/hagicode-core/src/PCode.ClaudeHelper/AI/Providers/OpenCodeCliProvider.cs
  • Runtime Coordinator: repos/hagicode-core/src/PCode.ClaudeHelper/AI/Providers/OpenCodeRuntimeCoordinator.cs
  • Конфигурация: repos/hagicode-core/src/PCode.ClaudeHelper/AI/Configuration/OpenCodeSettings.cs
  • Архив предложений: openspec/changes/archive/2026-03-*opencode*/

Заключение

Процесс интеграции OpenCode в HagiCode — это процесс постоянного наступания на грабли и постоянной оптимизации. От первоначальной модели отдельных процессов к общему runtime, от повторного использования внешних конечных точек к самоуправляемому runtime — каждое изменение архитектуры вызвано реальными потребностями. На самом деле это ничего, просто该踩的坑一个都没少踩.

Основной опыт три пункта:

  1. Совместное использование ресурсов очень важно: не слепо стремитесь к изоляции, общий runtime может значительно снизить накладные расходы ресурсов — иногда один человек наслаждается в одиночку хуже, чем все вместе используют
  2. Управление состоянием нужно быть осторожным: stateful сервисы нужно управлять самим, не зависеть от внешних конечных точек —毕竟 свои дела всё же делать самим более надёжно
  3. Восстановление ошибок нельзя упускать: механизм автоматического повтора может поднять устойчивость системы на новую ступень — упал — встань, ничего страшного

这套方案现在 в HagiCode работает стабильно, поддерживает восстановление сеансов, автоматический повтор, пересоздание runtime и другие функции. Если ваш проект также требует интеграции OpenCode, надеюсь, этот опыт поможет вам меньше свернуть не туда. Ведь… только свернув не туда узнаешь, где shortcut, только иногда узнаёшь и уже бесполезно.

Справочные материалы

开始使用 HagiCode

一次安装,几分钟上手

HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。