feat: встроенные агенты opencode (analyst, dev) через go:embed, распаковка по умолчанию в ./agents рядом с бинарём
All checks were successful
CI / test (push) Successful in 38s
CI / build-and-package (amd64, darwin) (push) Successful in 34s
CI / build-and-package (amd64, linux) (push) Successful in 33s
CI / build-and-package (amd64, windows) (push) Successful in 35s
CI / build-and-package (arm64, darwin) (push) Successful in 35s
CI / build-and-package (arm64, linux) (push) Successful in 34s
All checks were successful
CI / test (push) Successful in 38s
CI / build-and-package (amd64, darwin) (push) Successful in 34s
CI / build-and-package (amd64, linux) (push) Successful in 33s
CI / build-and-package (amd64, windows) (push) Successful in 35s
CI / build-and-package (arm64, darwin) (push) Successful in 35s
CI / build-and-package (arm64, linux) (push) Successful in 34s
This commit is contained in:
32
internal/agents/agents.go
Normal file
32
internal/agents/agents.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Package agents — встроенные агенты opencode (analyst, dev, ...).
|
||||||
|
//
|
||||||
|
// Агенты зашиты в бинарь через go:embed и распаковываются при запуске
|
||||||
|
// в каталог OPENCODE_CONFIG_DIR (по умолчанию ./agents рядом с бинарём),
|
||||||
|
// чтобы opencode находил их независимо от того, где запущен ratatoskr.
|
||||||
|
package agents
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed *.md
|
||||||
|
var FS embed.FS
|
||||||
|
|
||||||
|
// Names — имена распаковываемых агентов (файлы *.md, без .md).
|
||||||
|
//
|
||||||
|
// Держим в курсе: если добавить файл в каталог, он автоматически попадёт
|
||||||
|
// в FS, но для детерминированной распаковки список лучше дополнять здесь.
|
||||||
|
var Names = []string{"analyst", "dev"}
|
||||||
|
|
||||||
|
// WriteTo распаковывает всех встроенных агентов в каталог dir (создаёт его).
|
||||||
|
// Файлы перезаписываются — встроенная копия всегда актуальна.
|
||||||
|
func WriteTo(dir string) error {
|
||||||
|
for _, name := range Names {
|
||||||
|
data, err := FS.ReadFile(name + ".md")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := writeFile(dir, name+".md", data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
25
internal/agents/agents_test.go
Normal file
25
internal/agents/agents_test.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package agents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriteTo(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := WriteTo(dir); err != nil {
|
||||||
|
t.Fatalf("WriteTo: %v", err)
|
||||||
|
}
|
||||||
|
for _, name := range Names {
|
||||||
|
p := filepath.Join(dir, name+".md")
|
||||||
|
data, err := os.ReadFile(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read %s: %v", p, err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), "name: "+name) {
|
||||||
|
t.Errorf("%s: нет frontmatter name: %s", p, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
internal/agents/analyst.md
Normal file
35
internal/agents/analyst.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: analyst
|
||||||
|
description: Аналитик Ratatoskr — помогает сформулировать задачу разработки и возвращает структурированное решение в JSON
|
||||||
|
---
|
||||||
|
|
||||||
|
Ты — аналитик в конвейере Ratatoskr. Пользователь ставит задачу разработки. Ты помогаешь
|
||||||
|
продумать её до уровня готового задания для агента, который пишет код.
|
||||||
|
|
||||||
|
Тебе приходит промпт, в котором есть:
|
||||||
|
- переписка с пользователем (что уже обсудили);
|
||||||
|
- текущий черновик задачи (заполненные поля — title, goal, repo, why, ac; пустые — не заданы);
|
||||||
|
- режим принудительного предложения (если включён — не задавать вопросы, сразу предложить черновик).
|
||||||
|
|
||||||
|
ПРАВИЛА:
|
||||||
|
1. Пока неясны цель, репозиторий или критерии готовности — задай до 3 уточняющих вопросов
|
||||||
|
(phase="ask"). Не задавай лишних вопросов, если поле уже заполнено.
|
||||||
|
2. Когда данных достаточно (понятны цель, репозиторий, ≥2–3 проверяемых критерия) —
|
||||||
|
phase="propose" и в chat_reply краткое резюме черновика.
|
||||||
|
3. Если тема явно не про разработку кода или не подходит для агента-кодера — phase="abort"
|
||||||
|
с причиной.
|
||||||
|
4. Возвращай ВСЕГДА строго один JSON-объект без markdown-обрамления и без лишнего текста.
|
||||||
|
5. Меняй в JSON только те поля, которые надо изменить; незаполненные — пустой строкой.
|
||||||
|
|
||||||
|
Формат ответа:
|
||||||
|
{
|
||||||
|
"phase": "ask|propose|abort",
|
||||||
|
"title": "",
|
||||||
|
"goal": "",
|
||||||
|
"repo": "",
|
||||||
|
"why": "",
|
||||||
|
"ac": "",
|
||||||
|
"questions": ["вопрос 1", "вопрос 2"],
|
||||||
|
"chat_reply": "твой ответ пользователю (на русском, естественно)",
|
||||||
|
"abort_reason": "причина, если phase=abort, иначе пусто"
|
||||||
|
}
|
||||||
16
internal/agents/dev.md
Normal file
16
internal/agents/dev.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
name: dev
|
||||||
|
description: Разработчик Ratatoskr — реализует задачу в репозитории по готовым критериям
|
||||||
|
---
|
||||||
|
|
||||||
|
Ты — dev-агент в конвейере Ratatoskr. Получаешь готовое задание и реализуешь его в коде.
|
||||||
|
|
||||||
|
Тебе приходит промпт с задачей: название, цель, зачем, репозиторий, критерии готовности (AC).
|
||||||
|
|
||||||
|
ПРАВИЛА:
|
||||||
|
1. Изучи существующий код репозитория, прежде чем менять.
|
||||||
|
2. Реализуй задачу так, чтобы все acceptance criteria были выполнены.
|
||||||
|
3. Пользуйся инструментами opencode (чтение файлов, поиск, редактирование, запуск команд).
|
||||||
|
4. Не удаляй код без необходимости; следуй кодстайлу проекта.
|
||||||
|
5. Закоммить осмысленными атомарными коммитами.
|
||||||
|
6. По окончании работы верни краткий отчёт: что сделано, какие AC закрыты, что осталось.
|
||||||
18
internal/agents/embed.go
Normal file
18
internal/agents/embed.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package agents
|
||||||
|
|
||||||
|
// WriteTo распаковывает всех встроенных агентов в каталог dir.
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(dir, name string, data []byte) error {
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p := filepath.Join(dir, name)
|
||||||
|
if err := os.WriteFile(p, data, 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -8,10 +8,12 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/kamelion/ratatoskr-go/internal/agents"
|
||||||
"github.com/kamelion/ratatoskr-go/internal/analyst"
|
"github.com/kamelion/ratatoskr-go/internal/analyst"
|
||||||
"github.com/kamelion/ratatoskr-go/internal/chat"
|
"github.com/kamelion/ratatoskr-go/internal/chat"
|
||||||
"github.com/kamelion/ratatoskr-go/internal/chat/telegram"
|
"github.com/kamelion/ratatoskr-go/internal/chat/telegram"
|
||||||
@@ -46,6 +48,11 @@ func New(configPath string) (*App, error) {
|
|||||||
// SQLite для задач и трасс
|
// SQLite для задач и трасс
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Агенты opencode: если config_dir не задан — используем ./agents рядом с бинарём
|
||||||
|
if err := ensureAgentsDir(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
store, err := storage.Open(ctx, cfg.Paths.DB)
|
store, err := storage.Open(ctx, cfg.Paths.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrDBOpen, err)
|
return nil, fmt.Errorf("%w: %v", ErrDBOpen, err)
|
||||||
@@ -222,4 +229,25 @@ func (a *App) send(ctx context.Context, uid chat.UserID, text string) {
|
|||||||
if err := a.Router.Send(ctx, uid, chat.Message{Text: text}); err != nil {
|
if err := a.Router.Send(ctx, uid, chat.Message{Text: text}); err != nil {
|
||||||
log.Printf("app: send to %s: %v", uid, err)
|
log.Printf("app: send to %s: %v", uid, err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureAgentsDir определяет каталог с агентами opencode и распаковывает
|
||||||
|
// туда встроенных агентов (go:embed). Если config_dir не задан — использует
|
||||||
|
// ./agents рядом с бинарём. Встроенные агенты перезаписываются (всегда актуальны).
|
||||||
|
//
|
||||||
|
// idempotent: вызывается только из New.
|
||||||
|
func ensureAgentsDir(cfg *config.Config) error {
|
||||||
|
if cfg.OpenCode.ConfigDir == "" {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolve executable: %w", err)
|
||||||
|
}
|
||||||
|
// ./agents рядом с бинарём
|
||||||
|
cfg.OpenCode.ConfigDir = filepath.Join(filepath.Dir(exe), "agents")
|
||||||
|
}
|
||||||
|
if err := agents.WriteTo(cfg.OpenCode.ConfigDir); err != nil {
|
||||||
|
return fmt.Errorf("write agents to %s: %w", cfg.OpenCode.ConfigDir, err)
|
||||||
|
}
|
||||||
|
log.Printf("app: agents ensured in %s", cfg.OpenCode.ConfigDir)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user