mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 13:11:31 -10:00
* stage 01: web_search tool Adds a web_search tool alongside web_fetch: discovers URLs via a pluggable searchBackend (env-configured generic JSON backend; swappable for Exa/You/ Tavily/Brave) and formats ranked results as a numbered list. Registered in CoreNetworkTools(). Deviation from the stage spec: the spec said readOnlySafety, but the repo enforces (TestCoreNetworkToolsExposePromptMetadata) that every core network tool is prompt-gated network egress — so web_search matches web_fetch's posture instead of being auto-allowed. Necessary fallout from adding the tool: knownToolNames (+web_search), the network-tools metadata test (now covers both), and a stale 'unknown tool' fixture that had used web_search. * stage 02: LSP client foundation New internal/lsp package: a direct (self-spawned, stdio) Language Server Protocol client so unattended runs see real compiler diagnostics without an open editor. - client.go: JSON-RPC 2.0 with LSP Content-Length framing; concurrent Call/Notify, id-matched responses, server->client request auto-reply, notification handler, context cancellation, read-loop teardown. - server.go: process lifecycle (spawn via os/exec, initialize/initialized handshake, graceful shutdown+exit with kill fallback). Handshake factored out (performInitialize) for pipe-based testing. - registry.go: extension -> server command (.go/.ts/.tsx/.js/.jsx/.py/.rs), languageId map, PATH availability via exec.LookPath. - types.go: minimal LSP types + PathToURI/URIToPath (Windows-drive aware). Stdlib-only; tested over in-memory pipes with a stub server (no real gopls), including -race for the concurrent id router. * stage 03: LSP document sync + diagnostics Makes the LSP client produce actionable signal so ZERO can ask 'did my edit compile?' headlessly. - documents.go: per-server session with document lifecycle (didOpen/didChange full-sync/didClose), a URI->diagnostics store fed by publishDiagnostics, and WaitForDiagnostics with a publish-baseline + 300ms quiet debounce (servers never signal 'analysis complete'). - diagnostics.go: FormatDiagnostics (path:line:col: severity: message, 1-based), HasErrors, FilterBySeverity, severity labels. - manager.go: Manager — one lazily-started, reused server per language (keyed by binary), routes a file to the right server, Check(ctx,path,text) -> settled diagnostics, graceful Shutdown; unknown extension -> (nil,nil), not an error. Concurrency-safe; tested under -race over in-memory pipes with a stub server (no real gopls). FormatDiagnostics takes the path explicitly since a Diagnostic carries only a range, not its document. * wedge: address review on #175 (LSP robustness + web_search provider) - lsp/client.go: Close() now disables the client — Call/Notify reject after close instead of leaking a pending entry / writing to a dead conn. - lsp/documents.go: serialize same-document syncs through the Notify write via a per-URI lock and commit version/open only on success; drop delayed publishDiagnostics for a superseded (older positive) version; WaitForDiagnostics now reports whether a fresh publish arrived. - lsp/manager.go: Check returns (nil,nil) when no publish newer than baseline arrives (don't return a stale prior result for new text); Shutdown joins and returns per-server shutdown errors instead of always reporting success. - lsp/diagnostics.go: FormatDiagnostics collapses embedded whitespace so each diagnostic stays one physical line. - tools/web_search.go: forward ZERO_WEBSEARCH_PROVIDER in the request so the config knob isn't inert. Adds tests: close-disables-client, stale-version drop, multi-line message collapse, Shutdown error propagation, httptest backend (provider+parse). * wedge: degrade gracefully when an LSP server binary is missing (#175 review) Manager.Check translated a missing-binary start error (exec.ErrNotFound) into (nil,nil) so a configured extension on a machine without gopls/ pyright/rust-analyzer degrades exactly like an unsupported extension — LSP diagnostics are an opportunistic layer, not a hard dependency. Other start failures still surface. Adds a test injecting an exec.ErrNotFound starter. * wedge stage 04: self-correct loop (#176) * stage 04: self-correct loop After a successful mutating tool call, verify the changed files (LSP diagnostics + project tests) and feed failures back to the model to fix — bounded and autonomy-gated, so an unattended run fixes its own mistakes without looping forever. - internal/agent/selfcorrect.go: SelfCorrector with injectable diagnosticsChecker (lsp.Manager adapter) and projectVerifier (verify.DetectPlan+Run); AfterEdit produces a redacted, model-facing corrective message; per-run attempt ceiling; low autonomy reports without auto-attempting; medium/high auto-correct. - internal/agent/loop.go: invoke AfterEdit after a successful mutating tool call (ChangedFiles non-empty), appending feedback to messages. nil SelfCorrect (the default) is a no-op — read-only tools never trigger it. - internal/agent/types.go: add Options.SelfCorrect *SelfCorrector (the explicit options-struct change). Single-pass verify per call (the agent loop + model edits provide the multi- attempt cycle, not selfverify's same-command re-runs). Tested with fakes (no gopls/shell): fail-then-pass, max-attempts abort, read-only skip, low-autonomy report-only, LSP-only failure, secret redaction; -race clean. CLI activation (a --self-correct flag building the SelfCorrector in exec.go) is intentionally left out to honor this stage's file scope — the engine is exported and ready to wire. * wedge: propagate file-read error in self-correct LSP checker (#176 review) CodeRabbit/golangci-lint (nilerr) flagged lspDiagnosticsChecker.Check returning (nil, nil) after os.ReadFile fails. Propagate the error instead; the sole caller (inspect) already skips files whose Check errors, so a deleted/unreadable file still degrades quietly while future callers can decide. Adds a regression test. * wedge: surface non-degradable verify errors in self-correct (#175 review) SelfCorrector.inspect silently continued on every checker.Check error and ignored verifier errors, so a real LSP startup/sync failure, an unreadable changed file, or a failed plan detection produced report.Failed=false -> OutcomePassed with no signal. Manager.Check already degrades missing servers to (nil,nil) and DetectPlan returns an empty plan (not an error) when nothing is detected, so a non-nil error from either is a genuine failure. Record such errors in CorrectionReport.InspectErrors, fail the pass, and surface them in the model feedback. Adds regression tests for both paths. * wedge: drop third-party search-provider names from web_search comments Keep code/comments provider-neutral: describe the backend interface as 'any hosted search API' rather than naming specific services. * wedge: defer self-correct feedback until after the tool-call batch (#175 review) Self-correct feedback was appended as a user message inside the per-tool-call loop, so a multi-tool-call turn whose first (mutating) call failed verification produced: assistant tool_use batch -> tool_result 1 -> user feedback -> tool_result 2. A user message interleaved between tool_results breaks strict provider replay and later session replay/compaction. Accumulate feedback during the batch and append it once after the loop (same after-batch pattern already used for mid-run model escalation), so every advertised tool call keeps its tool_result contiguous. Adds a two-tool-call regression test asserting the feedback lands after both tool_results. * wedge: batch self-correct AfterEdit once per turn over union of changes (#175 review) Deferring only the feedback emission still ran SelfCorrect.AfterEdit once per mutating tool call, which (a) consumes the per-run attempt budget multiple times in a single turn and (b) verifies an intermediate edit that a later call in the same batch already superseded — AfterEdit is documented as one per-run instance holding attempt state, so this is a correctness issue, not just transcript shape. Aggregate every mutating tool's ChangedFiles during the batch and call AfterEdit once afterward over the deduped union, appending its feedback once (still after all tool_results, preserving contiguity). Adds dedupeStrings and a regression test asserting two writes in one turn consume exactly one attempt and emit one feedback message.
77 lines
2.1 KiB
Go
77 lines
2.1 KiB
Go
package lsp
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// severityLabel maps the LSP severity enum to a short human label. An unset
|
|
// severity (0) is treated as an error — the conservative choice for a tool whose
|
|
// job is to stop broken edits.
|
|
func severityLabel(severity DiagnosticSeverity) string {
|
|
switch severity {
|
|
case SeverityWarning:
|
|
return "warning"
|
|
case SeverityInformation:
|
|
return "info"
|
|
case SeverityHint:
|
|
return "hint"
|
|
default:
|
|
return "error"
|
|
}
|
|
}
|
|
|
|
// collapseWhitespace flattens a (possibly multi-line) diagnostic message to a
|
|
// single line so each formatted record stays "path:line:col: severity: message"
|
|
// and downstream line-based parsing isn't broken by embedded \n / \t.
|
|
func collapseWhitespace(message string) string {
|
|
return strings.Join(strings.Fields(message), " ")
|
|
}
|
|
|
|
// hasErrors reports whether any diagnostic is error severity.
|
|
func hasErrors(diags []Diagnostic) bool {
|
|
for _, diag := range diags {
|
|
if diag.Severity == SeverityError || diag.Severity == 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// FilterBySeverity returns only diagnostics at or above (numerically at or below,
|
|
// since Error=1 is most severe) the given severity.
|
|
func FilterBySeverity(diags []Diagnostic, minimum DiagnosticSeverity) []Diagnostic {
|
|
out := make([]Diagnostic, 0, len(diags))
|
|
for _, diag := range diags {
|
|
sev := diag.Severity
|
|
if sev == 0 {
|
|
sev = SeverityError
|
|
}
|
|
if sev <= minimum {
|
|
out = append(out, diag)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// FormatDiagnostics renders diagnostics as compact, model-readable lines:
|
|
//
|
|
// path:line:col: severity: message
|
|
//
|
|
// Lines/columns are converted from LSP's zero-based positions to the one-based
|
|
// form editors and compilers print. The path is passed in because a Diagnostic
|
|
// carries only a range, not its document.
|
|
func FormatDiagnostics(path string, diags []Diagnostic) string {
|
|
lines := make([]string, 0, len(diags))
|
|
for _, diag := range diags {
|
|
lines = append(lines, fmt.Sprintf("%s:%d:%d: %s: %s",
|
|
path,
|
|
diag.Range.Start.Line+1,
|
|
diag.Range.Start.Character+1,
|
|
severityLabel(diag.Severity),
|
|
collapseWhitespace(diag.Message),
|
|
))
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|