diff --git a/.serena/project.yml b/.serena/project.yml index fb466c8..91b393f 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -1,39 +1,6 @@ -# the name by which the project can be referenced within Serena +# the name by which the project can be referenced within Serena/when chatting with the LLM. project_name: "ratatoskr-go" - -# list of languages for which language servers are started; choose from: -# al angular ansible bash clojure -# cpp cpp_ccls crystal csharp csharp_omnisharp -# dart elixir elm erlang fortran -# fsharp go groovy haskell haxe -# hlsl html java json julia -# kotlin lean4 lua luau markdown -# matlab msl nix ocaml pascal -# perl php php_phpactor powershell python -# python_jedi python_ty r rego ruby -# ruby_solargraph rust scala scss solidity -# svelte swift systemverilog terraform toml -# typescript typescript_vts vue yaml zig -# (This list may be outdated. For the current list, see values of Language enum here: -# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py -# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) -# Note: -# - For C, use cpp -# - For JavaScript, use typescript -# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root) -# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm) -# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three) -# - For Free Pascal/Lazarus, use pascal -# Special requirements: -# Some languages require additional setup/installations. -# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers -# When using multiple languages, the first language server that supports a given file will be used for that file. -# The first language is the default language and the respective language server will be used as a fallback. -# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. -languages: -- go - # the encoding used by text files in the project # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings encoding: "utf-8" @@ -55,23 +22,19 @@ ignore_all_files_in_gitignore: true # advanced configuration option allowing to configure language server-specific options. # Maps the language key to the options. -# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. -# No documentation on options means no options are available. +# The settings are considered only if the project is trusted (see global configuration to define trusted projects). +# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings ls_specific_settings: {} -# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos). -# Paths can be absolute or relative to the project root. -# Each folder is registered as an LSP workspace folder, enabling language servers to discover -# symbols and references across package boundaries. -# Currently supported for: TypeScript. -# Example: -# additional_workspace_folders: -# - ../sibling-package -# - ../shared-lib -additional_workspace_folders: [] - # list of additional paths to ignore in this project. # Same syntax as gitignore, so you can use * and **. +# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases. +# Example: +# ignored_paths: +# - "examples/**" +# - ".worktrees/**" +# - "**/bin/**" +# - "**/obj/**" # Note: global ignored_paths from serena_config.yml are also applied additively. ignored_paths: [] @@ -131,3 +94,76 @@ read_only_memory_patterns: [] # Extends the list from the global configuration, merging the two lists. # Example: ["_archive/.*", "_episodes/.*"] ignored_memory_patterns: [] + +# list of additional workspace folder paths for cross-package reference support. +# Paths can be absolute or relative to the project root. +# Each folder is registered as an LSP workspace folder, enabling language servers to discover +# symbols and references across package boundaries, but these folders are not indexed by Serena, +# i.e. the respective symbols will not be found using Serena's symbol search tools. +# Example: +# additional_workspace_folders: +# - ../sibling-package +# - ../shared-lib +ls_additional_workspace_folders: [] + +# list of language servers to start when using the LSP backend; choose from: +# ada al angular ansible bash +# bsl clojure cpp cpp_ccls crystal +# csharp csharp_omnisharp cue dart deno +# elixir elm erlang fortran fsharp +# gdscript gleam go groovy haskell +# haxe hlsl html java json +# julia kotlin latex lean4 lua +# luau markdown matlab msl nextflow +# nix ocaml pascal perl php +# php_phpactor php_phpantom powershell python python_basedpyright +# python_jedi python_pyrefly python_ty qml r +# rego ruby ruby_solargraph rust scala +# scss solidity svelte swift systemverilog +# terraform toml typescript typescript_vts vue +# wolfram yaml zig +# (This list may be outdated; generated with scripts/print_language_list.py; +# For the current list, see values of the LanguageServerId enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py) +# For some languages, there are several alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root) +# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm) +# - For Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH) +# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three) +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some language servers require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple language servers, the first language server that supports a given file will be used for that file. +# The first language server is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +language_servers: +- go + +# list of workspace folder paths (LSP backend only). +# These folders will be used to build up Serena's symbol index. +# Paths must be within the project root and should thus be relative to the project root. +# Furthermore, the paths should not be filtered by ignore settings. +# Default setting: The entire project root folder (".") is considered. +# In (large) monorepos, this can be used to index only subfolders of the project root, e.g. +# ls_workspace_folders: +# - "./subproject1" +# - "./subproject2" +ls_workspace_folders: +- . + +# optional shell command to run before the language backend (LSP or JetBrains) is initialised. +# the command runs in the project root directory and is only executed if the project is trusted +# (see trusted_project_path_patterns in the global configuration). +# serena waits for the command to exit: a non-zero exit code is logged as an error but does not +# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety +# backstop for non-terminating commands; on expiry the process is killed and activation continues. +# example: activation_command: "npx nx run-many -t build" +activation_command: + +# maximum time in seconds to wait for activation_command to complete before killing it (default 180s). +# must be a positive number. +activation_command_timeout: 180.0 diff --git a/internal/opencode/client.go b/internal/opencode/client.go index fad9816..0faaa3b 100644 --- a/internal/opencode/client.go +++ b/internal/opencode/client.go @@ -292,6 +292,22 @@ func newestAssistant(msgs []v2Message, since int64) (*v2Message, int) { return newest, count } +// progressOf — «живой» прогресс новых assistant-сообщений: число контент-партов +// (text/reasoning/tool) + суммарная длина их текста. Растёт во время стриминга, +// когда один и тот же парт увеличивается (и при reasoning), — это и есть +// сигнал, что LLM работает, а не висит. +func progressOf(msgs []v2Message, since int64) (parts, textLen int) { + for _, m := range assistantSince(msgs, since) { + for _, p := range m.Content { + parts++ + if p.Type == "text" || p.Type == "reasoning" { + textLen += len(p.Text) + } + } + } + return +} + // assistantText объединяет text-парты новых assistant-сообщений в хронологическом // порядке (сообщения приходят новейшими первыми → идём с конца). func assistantText(msgs []v2Message, since int64) []string { diff --git a/internal/opencode/client_test.go b/internal/opencode/client_test.go index 50dc6c7..c8d03d5 100644 --- a/internal/opencode/client_test.go +++ b/internal/opencode/client_test.go @@ -6,6 +6,7 @@ import ( "errors" "net/http" "net/http/httptest" + "strings" "testing" "time" ) @@ -16,7 +17,10 @@ import ( // - нормальный: Prompt ставит active=false и в messages кладётся финальное // assistant-сообщение (verdictText) → Runner собирает вердикт; // - blockPrompt: «агент завис» — active=true всегда, сообщений нет → idle abort; -// - failCreate / failMessages — имитация ошибок. +// - failCreate / failMessages — имитация ошибок; +// - growStream: стрим одного растущего парта — текст/reasoning растёт с +// каждым опросом GET /message (streamPolls раз), active=true, затем +// active=false + финальное завершённое сообщение. type fakeAPIServer struct { sessionID string created bool @@ -28,6 +32,14 @@ type fakeAPIServer struct { failMessages bool createdModel *ModelRef // модель, полученная на POST /api/session promptCalls int + + // streamGrow: стрим одного растущего парта — текст/reasoning растёт с + // каждым опросом GET /message, active=true, пока messageCalls не дойдёт до + // streamPolls; затем active=false + финальное завершённое сообщение. + streamGrow bool + streamReasoning bool // растущий парт — reasoning вместо text + streamPolls int // сколько опросов длится «стрим» до завершения + messageCalls int } func (f *fakeAPIServer) handler() http.Handler { @@ -96,6 +108,27 @@ func (f *fakeAPIServer) handler() http.Handler { http.Error(w, "db error", http.StatusInternalServerError) return } + if f.streamGrow { + f.messageCalls++ + now := time.Now().UnixMilli() + done := f.messageCalls >= f.streamPolls + msg := v2Message{ID: "msg_stream", Type: "assistant", Time: v2Time{Created: &now}} + switch { + case done: + msg.Content = []v2Part{{Type: "text", Text: "done-stream"}} + msg.Finish = "end_turn" + msg.Time.Completed = &now + f.active = false + case f.streamReasoning: + msg.Content = []v2Part{{Type: "reasoning", Text: strings.Repeat("r", f.messageCalls)}} + f.active = true + default: + msg.Content = []v2Part{{Type: "text", Text: strings.Repeat("x", f.messageCalls)}} + f.active = true + } + writeJSON(w, map[string]any{"data": []v2Message{msg}}) + return + } msgs := f.messages if msgs == nil && f.verdictText != "" && !f.blockPrompt { msgs = []v2Message{f.assistantMsg(f.verdictText)} diff --git a/internal/opencode/runner.go b/internal/opencode/runner.go index 41683b9..a5a946c 100644 --- a/internal/opencode/runner.go +++ b/internal/opencode/runner.go @@ -120,9 +120,10 @@ func (r *Runner) awaitVerdict(ctx context.Context, c *Client, model *ModelRef, s admittedAt = adm.TimeCreated } - // Прогресс = число text-партов в новых assistant-сообщениях. Рост сбрасывает - // idle-таймер (LLM стримит = жив). - lastCount := -1 + // Прогресс = число контент-партов + суммарная длина их текста в новых + // assistant-сообщениях (progressOf). Рост сбрасывает idle-таймер: LLM + // стримит (даже в один растущий text-парт) или думает (reasoning) = жив. + lastParts, lastTextLen := -1, -1 lastProgress := time.Now() launch := time.Now() @@ -164,10 +165,11 @@ func (r *Runner) awaitVerdict(ctx context.Context, c *Client, model *ModelRef, s return nil, err } - cur, count := newestAssistant(msgs, admittedAt) - if count != lastCount { + cur, _ := newestAssistant(msgs, admittedAt) + parts, textLen := progressOf(msgs, admittedAt) + if parts != lastParts || textLen != lastTextLen { lastProgress = time.Now() - lastCount = count + lastParts, lastTextLen = parts, textLen } now := time.Now() if now.Sub(lastProgress) > r.IdleTimeout { diff --git a/internal/opencode/runner_test.go b/internal/opencode/runner_test.go index 5791131..efdf24c 100644 --- a/internal/opencode/runner_test.go +++ b/internal/opencode/runner_test.go @@ -70,6 +70,46 @@ func TestRun_IdleTimeout(t *testing.T) { } } +func TestRun_StreamingGrowth(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + dir := t.TempDir() + // стрим: один text-парт растёт с каждым опросом (дольше, чем idle timeout), + // но модель жива → idle НЕ должен сработать. + f := &fakeAPIServer{streamGrow: true, streamPolls: 30} + p, _ := fakePool(t, f, dir) + + r := &Runner{Pool: p, IdleTimeout: 60 * time.Millisecond, + PollInterval: 5 * time.Millisecond, Stdout: io.Discard} + res, err := r.Run(context.Background(), "task", dir, "dev", "") + if err != nil { + t.Fatalf("Run err: %v", err) + } + if res.RC != 0 { + t.Errorf("RC = %d, want 0 (растущий стрим не должен считаться hung)", res.RC) + } + if !contains(res.Stdout, "done-stream") { + t.Errorf("Stdout = %q, want contain done-stream", res.Stdout) + } +} + +func TestRun_ReasoningGrowth(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + dir := t.TempDir() + // стрим: растёт только reasoning-парт (текста нет) — тоже живая активность. + f := &fakeAPIServer{streamGrow: true, streamReasoning: true, streamPolls: 30} + p, _ := fakePool(t, f, dir) + + r := &Runner{Pool: p, IdleTimeout: 60 * time.Millisecond, + PollInterval: 5 * time.Millisecond, Stdout: io.Discard} + res, err := r.Run(context.Background(), "task", dir, "dev", "") + if err != nil { + t.Fatalf("Run err: %v", err) + } + if res.RC != 0 { + t.Errorf("RC = %d, want 0 (растущий reasoning не должен считаться hung)", res.RC) + } +} + func TestRun_ContextCancel(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) dir := t.TempDir()