| 1 | package lsp |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | |
| 8 | "reasonix/internal/tool" |
| 9 | ) |
| 10 | |
| 11 | // Tools adapts the manager's read-only queries to the tool.Tool interface. They |
| 12 | // report ReadOnly=true, so a batch of them rides the agent's parallel dispatch |
| 13 | // and shares one server per language. |
| 14 | func Tools(m *Manager) []tool.Tool { |
| 15 | return []tool.Tool{ |
| 16 | posTool{m, "lsp_definition", "Jump to where a symbol is defined. Give the file, the 1-based line the symbol appears on, and the symbol text itself.", m.Definition}, |
| 17 | posTool{m, "lsp_references", "List every reference to a symbol across the workspace. Give the file, the 1-based line, and the symbol text.", m.References}, |
| 18 | posTool{m, "lsp_hover", "Show the type signature and documentation for a symbol. Give the file, the 1-based line, and the symbol text.", m.Hover}, |
| 19 | diagTool{m}, |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | type posArgs struct { |
| 24 | File string `json:"file"` |
| 25 | Line int `json:"line"` |
| 26 | Symbol string `json:"symbol"` |
| 27 | } |
| 28 | |
| 29 | type posTool struct { |
| 30 | m *Manager |
| 31 | name string |
| 32 | desc string |
| 33 | fn func(context.Context, string, int, string) (string, error) |
| 34 | } |
| 35 | |
| 36 | func (t posTool) Name() string { return t.name } |
| 37 | func (t posTool) Description() string { return t.desc } |
| 38 | func (t posTool) ReadOnly() bool { return true } |
| 39 | |
| 40 | // SnipHint keeps a long head of definitions/references and a short tail: the |
| 41 | // first results are the ones the model acts on. |
| 42 | func (posTool) SnipHint() tool.SnipHint { |
| 43 | return tool.SnipHint{Head: 60, Tail: 10, HeadChars: 10000, TailChars: 1500} |
| 44 | } |
| 45 | func (t posTool) Schema() json.RawMessage { |
| 46 | return json.RawMessage(`{ |
| 47 | "type":"object", |
| 48 | "properties":{ |
| 49 | "file":{"type":"string","description":"Path to the source file, relative to the workspace root or absolute."}, |
| 50 | "line":{"type":"integer","description":"1-based line number the symbol appears on."}, |
| 51 | "symbol":{"type":"string","description":"The exact symbol text on that line, e.g. \"executeBatch\". Used to locate the column."} |
| 52 | }, |
| 53 | "required":["file","line","symbol"] |
| 54 | }`) |
| 55 | } |
| 56 | |
| 57 | func (t posTool) Execute(ctx context.Context, args json.RawMessage) (string, error) { |
| 58 | var p posArgs |
| 59 | if err := json.Unmarshal(args, &p); err != nil { |
| 60 | return "", fmt.Errorf("invalid args: %w", err) |
| 61 | } |
| 62 | if p.File == "" || p.Symbol == "" || p.Line < 1 { |
| 63 | return "", fmt.Errorf("file, line (>=1) and symbol are required") |
| 64 | } |
| 65 | return t.fn(ctx, p.File, p.Line, p.Symbol) |
| 66 | } |
| 67 | |
| 68 | type diagTool struct{ m *Manager } |
| 69 | |
| 70 | func (diagTool) Name() string { return "lsp_diagnostics" } |
| 71 | func (diagTool) ReadOnly() bool { return true } |
| 72 | |
| 73 | // SnipHint keeps a long head of diagnostics and a short tail. |
| 74 | func (diagTool) SnipHint() tool.SnipHint { |
| 75 | return tool.SnipHint{Head: 60, Tail: 10, HeadChars: 10000, TailChars: 1500} |
| 76 | } |
| 77 | func (diagTool) Description() string { |
| 78 | return "Report compiler/linter diagnostics (errors, warnings) for a file from its language server. Use after editing to check the change compiles." |
| 79 | } |
| 80 | func (diagTool) Schema() json.RawMessage { |
| 81 | return json.RawMessage(`{"type":"object","properties":{"file":{"type":"string","description":"Path to the source file, relative to the workspace root or absolute."}},"required":["file"]}`) |
| 82 | } |
| 83 | |
| 84 | func (t diagTool) Execute(ctx context.Context, args json.RawMessage) (string, error) { |
| 85 | var p struct { |
| 86 | File string `json:"file"` |
| 87 | } |
| 88 | if err := json.Unmarshal(args, &p); err != nil { |
| 89 | return "", fmt.Errorf("invalid args: %w", err) |
| 90 | } |
| 91 | if p.File == "" { |
| 92 | return "", fmt.Errorf("file is required") |
| 93 | } |
| 94 | return t.m.Diagnostics(ctx, p.File) |
| 95 | } |
| 96 |