fix(opencode): агенты — в <worktree>/.opencode/agent, OPENCODE_CONFIG_DIR больше не выставляем
OPENCODE_CONFIG_DIR в opencode v1.18.18 перенаправляет Global.Path.config (global.ts: config = OPENCODE_CONFIG_DIR ?? ~/.config/opencode), из-за чего глобальный конфиг (модель/провайдеры, напр. tokentool) не загружался и opencode уходил в fallback-модель. Агенты открывались, т.к. OPENCODE_CONFIG_DIR дополнительно сканируется как каталог для agent/*.md. Теперь агенты распаковываются в <worktree>/.opencode/agent/*.md, где opencode находит их через project-каталог .opencode (paths.ts, cwd=worktree). Глобальный конфиг не трогаем вовсе. - internal/opencode: удалены Server.ConfigDir/Pool.ConfigDir и env OPENCODE_CONFIG_DIR - internal/config: удалено поле OpenCodeCfg.ConfigDir (config_dir) - internal/app: ensureAgentsDir пишет в <worktree>/.opencode/agent - internal/agents: WriteTo(dir) → dir/agent/*.md - README/config.yaml.example/memory обновлены
This commit is contained in:
@@ -46,7 +46,7 @@ docs/ ui-spec.md — спека Fyne UI (слои, event-bus, fyne.Do
|
|||||||
|
|
||||||
- Интеграция с субагентами — через headless `opencode serve`, **v2 API** (`/api/*`). Версия opencode >= 1.18.18.
|
- Интеграция с субагентами — через headless `opencode serve`, **v2 API** (`/api/*`). Версия opencode >= 1.18.18.
|
||||||
- **Модель — только глобальный конфиг opencode.** Ratatoskr модель не выбирает и про неё не знает: opencode сам берёт модель по умолчанию из своего глобального конфига. Код opencode-конфиг не читает (config.go удалён).
|
- **Модель — только глобальный конфиг opencode.** Ratatoskr модель не выбирает и про неё не знает: opencode сам берёт модель по умолчанию из своего глобального конфига. Код opencode-конфиг не читает (config.go удалён).
|
||||||
- **Свой агент:** при создании сессии в `POST /api/session` передаётся `agent` (analyst/dev/reviewer/chat/postmortem) из встроенных определений (`internal/agents`, распаковка в `OPENCODE_CONFIG_DIR`).
|
- **Свой агент:** при создании сессии в `POST /api/session` передаётся `agent` (analyst/dev/reviewer/chat/postmortem) из встроенных определений. Агенты распаковываются в `<worktree>/.opencode/agent/*.md` (opencode находит через project-каталог `.opencode`); `OPENCODE_CONFIG_DIR` НЕ выставлять — он перенаправляет Global.Path.config и ломает глобальный конфиг (модель/провайдеры) → падение в fallback.
|
||||||
- **Поллинг вердикта:** `POST /api/session/:id/prompt` (durable admit) → `GET /api/session/:id/message?order=desc&limit=200` (новые assistant-сообщения, текст в `content[].type=="text"`) → завершение = `GET /api/session/active` без сессии + финальное assistant-сообщение, стабильное `settlePolls=2` опроса. `POST .../interrupt` вместо abort.
|
- **Поллинг вердикта:** `POST /api/session/:id/prompt` (durable admit) → `GET /api/session/:id/message?order=desc&limit=200` (новые assistant-сообщения, текст в `content[].type=="text"`) → завершение = `GET /api/session/active` без сессии + финальное assistant-сообщение, стабильное `settlePolls=2` опроса. `POST .../interrupt` вместо abort.
|
||||||
|
|
||||||
## Контракты (не ломать)
|
## Контракты (не ломать)
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ telegram:
|
|||||||
chat_id: "..." # TG_CHAT_ID (обязательно, если enabled)
|
chat_id: "..." # TG_CHAT_ID (обязательно, если enabled)
|
||||||
opencode:
|
opencode:
|
||||||
bin: "opencode"
|
bin: "opencode"
|
||||||
config_dir: "./agents" # каталог, куда распаковываются встроенные агенты
|
|
||||||
paths:
|
paths:
|
||||||
db: "./ratatoskr.db" # дефолт; резолвится от каталога бинаря (не от cwd)
|
db: "./ratatoskr.db" # дефолт; резолвится от каталога бинаря (не от cwd)
|
||||||
worktree: "./worktrees" # то же правило «всё рядом с .exe»
|
worktree: "./worktrees" # то же правило «всё рядом с .exe»
|
||||||
@@ -144,8 +143,10 @@ update:
|
|||||||
глобального конфига (`~/.config/opencode/opencode.jsonc`). Наш код конфиг
|
глобального конфига (`~/.config/opencode/opencode.jsonc`). Наш код конфиг
|
||||||
opencode не читает.
|
opencode не читает.
|
||||||
- **Свой агент.** При создании сессии в `POST /api/session` передаётся имя
|
- **Свой агент.** При создании сессии в `POST /api/session` передаётся имя
|
||||||
встроенного агента ratatoskr (analyst/dev/reviewer/chat/postmortem), который
|
встроенного агента ratatoskr (analyst/dev/reviewer/chat/postmortem). Агенты
|
||||||
распаковывается в каталог `OPENCODE_CONFIG_DIR` (см. `internal/agents`).
|
распаковываются в `<worktree>/.opencode/agent/*.md`, где opencode находит
|
||||||
|
их через project-каталог `.opencode` (см. `internal/agents`); глобальный
|
||||||
|
конфиг при этом не трогается.
|
||||||
- **Поллинг вердикта.** Промпт отправляется неблокирующе (`POST .../prompt` →
|
- **Поллинг вердикта.** Промпт отправляется неблокирующе (`POST .../prompt` →
|
||||||
durable admit), вердикт собирается из новых assistant-сообщений
|
durable admit), вердикт собирается из новых assistant-сообщений
|
||||||
(`GET .../message`); завершение ответа — сессия ушла из активных дренажей
|
(`GET .../message`); завершение ответа — сессия ушла из активных дренажей
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ telegram:
|
|||||||
# Всё ниже — опционально, показаны дефолты:
|
# Всё ниже — опционально, показаны дефолты:
|
||||||
# opencode:
|
# opencode:
|
||||||
# bin: "opencode"
|
# bin: "opencode"
|
||||||
# config: "/путь/к/opencode.json" # файл-конфиг модели (OPENCODE_CONFIG)
|
# config: "/путь/к/opencode.json" # файл-конфиг модели (OPENCODE_CONFIG); пусто = глобальный конфиг opencode
|
||||||
# config_dir: "./agents" # каталог с агентами (OPENCODE_CONFIG_DIR)
|
|
||||||
# hard_timeout: "20m"
|
# hard_timeout: "20m"
|
||||||
# idle_timeout: "5m"
|
# idle_timeout: "5m"
|
||||||
|
#
|
||||||
|
# Агенты (analyst/dev/reviewer/...) распаковываются автоматически в
|
||||||
|
# <worktree>/.opencode/agent/*.md; конфиг opencode для этого не нужен.
|
||||||
|
|
||||||
# git (источник репозиториев для git clone)
|
# git (источник репозиториев для git clone)
|
||||||
# base_url: "http://gitea.hal9000.home" # базовый URL git-хоста (обязательно для клонирования)
|
# base_url: "http://gitea.hal9000.home" # базовый URL git-хоста (обязательно для клонирования)
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
// Package agents — встроенные агенты opencode (analyst, dev, ...).
|
// Package agents — встроенные агенты opencode (analyst, dev, ...).
|
||||||
//
|
//
|
||||||
// Агенты зашиты в бинарь через go:embed и распаковываются при запуске
|
// Агенты зашиты в бинарь через go:embed и распаковываются при запуске
|
||||||
// в каталог OPENCODE_CONFIG_DIR (по умолчанию ./agents рядом с бинарём),
|
// в project-каталог <worktree>/.opencode/agent/*.md. opencode при serve
|
||||||
// чтобы opencode находил их независимо от того, где запущен ratatoskr.
|
// (cwd = worktree) обнаруживает .opencode поднятием вверх (ConfigPaths) и
|
||||||
//
|
// находит агентов по шаблону {agent,agents}/**/*.md. Глобальный конфиг
|
||||||
// opencode сканирует OPENCODE_CONFIG_DIR как каталог .opencode: агенты
|
// opencode остаётся нетронутым (OPENCODE_CONFIG_DIR не выставляем).
|
||||||
// ищутся в поддиректории agents/ (мн.ч.), а не в корне. Поэтому распаковка
|
|
||||||
// идёт в <dir>/agents/*.md.
|
|
||||||
package agents
|
package agents
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -23,10 +21,10 @@ var FS embed.FS
|
|||||||
// в FS, но для детерминированной распаковки список лучше дополнять здесь.
|
// в FS, но для детерминированной распаковки список лучше дополнять здесь.
|
||||||
var Names = []string{"analyst", "dev", "reviewer", "chat", "postmortem"}
|
var Names = []string{"analyst", "dev", "reviewer", "chat", "postmortem"}
|
||||||
|
|
||||||
// WriteTo распаковывает всех встроенных агентов в каталог dir/agents
|
// WriteTo распаковывает всех встроенных агентов в каталог dir/agent
|
||||||
// (создаёт его). Файлы перезаписываются — встроенная копия всегда актуальна.
|
// (создаёт его). Файлы перезаписываются — встроенная копия всегда актуальна.
|
||||||
func WriteTo(dir string) error {
|
func WriteTo(dir string) error {
|
||||||
agentsDir := filepath.Join(dir, "agents")
|
agentsDir := filepath.Join(dir, "agent")
|
||||||
for _, name := range Names {
|
for _, name := range Names {
|
||||||
data, err := FS.ReadFile(name + ".md")
|
data, err := FS.ReadFile(name + ".md")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ func TestWriteTo(t *testing.T) {
|
|||||||
t.Fatalf("WriteTo: %v", err)
|
t.Fatalf("WriteTo: %v", err)
|
||||||
}
|
}
|
||||||
for _, name := range Names {
|
for _, name := range Names {
|
||||||
// opencode ищет агентов в поддиректории agents/, как у .opencode
|
// opencode ищет агентов в поддиректории agent/ (или agents/) внутри .opencode
|
||||||
p := filepath.Join(dir, "agents", name+".md")
|
p := filepath.Join(dir, "agent", name+".md")
|
||||||
data, err := os.ReadFile(p)
|
data, err := os.ReadFile(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("read %s: %v", p, err)
|
t.Fatalf("read %s: %v", p, err)
|
||||||
|
|||||||
@@ -157,7 +157,6 @@ func New(configPath, version, updateToken string, noUI bool) (*App, error) {
|
|||||||
ocPool := opencode.NewPool(cfg.Paths.Worktree)
|
ocPool := opencode.NewPool(cfg.Paths.Worktree)
|
||||||
ocPool.Bin = cfg.OpenCode.Bin
|
ocPool.Bin = cfg.OpenCode.Bin
|
||||||
ocPool.Config = cfg.OpenCode.Config
|
ocPool.Config = cfg.OpenCode.Config
|
||||||
ocPool.ConfigDir = cfg.OpenCode.ConfigDir
|
|
||||||
ocPool.DBPath = cfg.OpenCode.DBPath
|
ocPool.DBPath = cfg.OpenCode.DBPath
|
||||||
ocPool.Host = cfg.OpenCode.Serve.Hostname
|
ocPool.Host = cfg.OpenCode.Serve.Hostname
|
||||||
ocPool.BasePort = cfg.OpenCode.Serve.Port
|
ocPool.BasePort = cfg.OpenCode.Serve.Port
|
||||||
@@ -665,23 +664,21 @@ func (a *App) handleUpdateCmd(ctx context.Context, uid chat.UserID) {
|
|||||||
// сюда не возвращаемся — SwapAndRestart завершил процесс (os.Exit)
|
// сюда не возвращаемся — SwapAndRestart завершил процесс (os.Exit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureAgentsDir определяет каталог с агентами opencode и распаковывает
|
// ensureAgentsDir распаковывает встроенных агентов opencode (go:embed) в
|
||||||
// туда встроенных агентов (go:embed). Если config_dir не задан — использует
|
// project-каталог <worktree>/.opencode/agent/*.md. opencode при serve (cwd =
|
||||||
// ./agents рядом с бинарём. Встроенные агенты перезаписываются (всегда актуальны).
|
// worktree) обнаруживает .opencode поднятием вверх (paths.ts) и сам находит
|
||||||
|
// агентов. Глобальный конфиг opencode (модель/провайдеры) не трогаем и
|
||||||
|
// OPENCODE_CONFIG_DIR не выставляем.
|
||||||
//
|
//
|
||||||
// idempotent: вызывается только из New.
|
// idempotent: вызывается только из New.
|
||||||
func ensureAgentsDir(cfg *config.Config) error {
|
func ensureAgentsDir(cfg *config.Config) error {
|
||||||
if cfg.OpenCode.ConfigDir == "" {
|
if cfg.Paths.Worktree == "" {
|
||||||
exe, err := os.Executable()
|
return fmt.Errorf("paths.worktree не задан — некуда раскладывать агентов")
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("resolve executable: %w", err)
|
|
||||||
}
|
}
|
||||||
// ./agents рядом с бинарём
|
base := filepath.Join(cfg.Paths.Worktree, ".opencode")
|
||||||
cfg.OpenCode.ConfigDir = filepath.Join(filepath.Dir(exe), "agents")
|
if err := agents.WriteTo(base); err != nil {
|
||||||
|
return fmt.Errorf("write agents to %s: %w", base, err)
|
||||||
}
|
}
|
||||||
if err := agents.WriteTo(cfg.OpenCode.ConfigDir); err != nil {
|
log.Printf("app: agents ensured in %s", base)
|
||||||
return fmt.Errorf("write agents to %s: %w", cfg.OpenCode.ConfigDir, err)
|
|
||||||
}
|
|
||||||
log.Printf("app: agents ensured in %s", cfg.OpenCode.ConfigDir)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,34 +146,6 @@ log:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoad_OpenCodeConfigDir(t *testing.T) {
|
|
||||||
t.Setenv("TG_TOKEN", "tok")
|
|
||||||
t.Setenv("TG_CHAT_ID", "42")
|
|
||||||
|
|
||||||
// без config_dir — пусто
|
|
||||||
yaml := `telegram:
|
|
||||||
token: "${TG_TOKEN}"
|
|
||||||
chat_id: "${TG_CHAT_ID}"
|
|
||||||
`
|
|
||||||
cfg, err := Load(writeCfg(t, yaml))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Load: %v", err)
|
|
||||||
}
|
|
||||||
if cfg.OpenCode.ConfigDir != "" {
|
|
||||||
t.Errorf("config_dir = %q, want пусто", cfg.OpenCode.ConfigDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// через env OPENCODE_CONFIG_DIR
|
|
||||||
t.Setenv("OPENCODE_CONFIG_DIR", "/opt/agents")
|
|
||||||
cfg, err = Load(writeCfg(t, yaml))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Load: %v", err)
|
|
||||||
}
|
|
||||||
if cfg.OpenCode.ConfigDir != "/opt/agents" {
|
|
||||||
t.Errorf("config_dir = %q, want /opt/agents", cfg.OpenCode.ConfigDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoad_EnvOverride(t *testing.T) {
|
func TestLoad_EnvOverride(t *testing.T) {
|
||||||
t.Setenv("TG_TOKEN", "tok")
|
t.Setenv("TG_TOKEN", "tok")
|
||||||
t.Setenv("TG_CHAT_ID", "42")
|
t.Setenv("TG_CHAT_ID", "42")
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ type OpenCodeCfg struct {
|
|||||||
Bin string `yaml:"bin" default:"opencode"`
|
Bin string `yaml:"bin" default:"opencode"`
|
||||||
DBPath string `yaml:"db_path" default:""`
|
DBPath string `yaml:"db_path" default:""`
|
||||||
Config string `yaml:"config" env:"OPENCODE_CONFIG"`
|
Config string `yaml:"config" env:"OPENCODE_CONFIG"`
|
||||||
ConfigDir string `yaml:"config_dir" env:"OPENCODE_CONFIG_DIR"`
|
|
||||||
HardTimeout Duration `yaml:"hard_timeout" default:"20m"`
|
HardTimeout Duration `yaml:"hard_timeout" default:"20m"`
|
||||||
IdleTimeout Duration `yaml:"idle_timeout" default:"5m"`
|
IdleTimeout Duration `yaml:"idle_timeout" default:"5m"`
|
||||||
PollMs Duration `yaml:"poll_ms" default:"2s"`
|
PollMs Duration `yaml:"poll_ms" default:"2s"`
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
type Pool struct {
|
type Pool struct {
|
||||||
Bin string
|
Bin string
|
||||||
Config string
|
Config string
|
||||||
ConfigDir string
|
|
||||||
DBPath string
|
DBPath string
|
||||||
Host string
|
Host string
|
||||||
BasePort int
|
BasePort int
|
||||||
@@ -74,7 +73,6 @@ func (p *Pool) EnsureRoot(ctx context.Context) error {
|
|||||||
s := &Server{
|
s := &Server{
|
||||||
Bin: p.Bin,
|
Bin: p.Bin,
|
||||||
Config: p.Config,
|
Config: p.Config,
|
||||||
ConfigDir: p.ConfigDir,
|
|
||||||
DBPath: p.DBPath,
|
DBPath: p.DBPath,
|
||||||
Host: p.Host,
|
Host: p.Host,
|
||||||
Password: p.Password,
|
Password: p.Password,
|
||||||
@@ -104,7 +102,6 @@ func (p *Pool) Ensure(ctx context.Context, dir string) (*Server, error) {
|
|||||||
s := &Server{
|
s := &Server{
|
||||||
Bin: p.Bin,
|
Bin: p.Bin,
|
||||||
Config: p.Config,
|
Config: p.Config,
|
||||||
ConfigDir: p.ConfigDir,
|
|
||||||
DBPath: p.DBPath,
|
DBPath: p.DBPath,
|
||||||
Host: p.Host,
|
Host: p.Host,
|
||||||
Password: p.Password,
|
Password: p.Password,
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
Bin string // путь к opencode (по умолчанию "opencode")
|
Bin string // путь к opencode (по умолчанию "opencode")
|
||||||
Config string // OPENCODE_CONFIG
|
Config string // OPENCODE_CONFIG
|
||||||
ConfigDir string // OPENCODE_CONFIG_DIR
|
|
||||||
DBPath string // рабочая БД сервера (передам env, если задана)
|
DBPath string // рабочая БД сервера (передам env, если задана)
|
||||||
|
|
||||||
Host string // hostname для прослушивания
|
Host string // hostname для прослушивания
|
||||||
@@ -127,12 +126,13 @@ func (s *Server) serveCmd(ctx context.Context) *exec.Cmd {
|
|||||||
env := append(os.Environ(),
|
env := append(os.Environ(),
|
||||||
"OPENCODE_DISABLE_AUTOUPDATE=1",
|
"OPENCODE_DISABLE_AUTOUPDATE=1",
|
||||||
"OPENCODE_DISABLE_MODELS_FETCH=1")
|
"OPENCODE_DISABLE_MODELS_FETCH=1")
|
||||||
|
// Агенты (analyst/dev/...) opencode находит сам через project-каталог
|
||||||
|
// .opencode (см. internal/agents); OPENCODE_CONFIG_DIR не выставляем,
|
||||||
|
// чтобы не перенаправлять Global.Path.config и сохранить глобальный
|
||||||
|
// конфиг opencode (модель/провайдеры).
|
||||||
if s.Config != "" {
|
if s.Config != "" {
|
||||||
env = append(env, "OPENCODE_CONFIG="+s.Config)
|
env = append(env, "OPENCODE_CONFIG="+s.Config)
|
||||||
}
|
}
|
||||||
if s.ConfigDir != "" {
|
|
||||||
env = append(env, "OPENCODE_CONFIG_DIR="+s.ConfigDir)
|
|
||||||
}
|
|
||||||
if s.DBPath != "" {
|
if s.DBPath != "" {
|
||||||
env = append(env, "OPENCODE_DB="+s.DBPath)
|
env = append(env, "OPENCODE_DB="+s.DBPath)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user