Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion cagent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,8 @@
"todo",
"fetch",
"api",
"a2a"
"a2a",
"lsp"
]
},
"instruction": {
Expand Down Expand Up @@ -516,6 +517,22 @@
}
}
},
{
"allOf": [
{
"properties": {
"type": {
"const": "lsp"
}
}
},
{
"required": [
"command"
]
}
]
},
{
"allOf": [
{
Expand Down
32 changes: 21 additions & 11 deletions examples/gopher.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,39 @@ agents:
**Workflow:**
1. **Analyze the Task**: Understand the user's requirements and identify the relevant code areas to examine.

2. **Code Examination**:
- Search for relevant code files and functions
- Analyze code structure and dependencies
- Identify potential areas for modification
2. **Code Examination**:
- Use `lsp_workspace_symbols` to find types, functions, and variables by name
- Use `lsp_document_symbols` to understand a file's structure
- Use `lsp_hover` to get type signatures and documentation
- Use `lsp_definition` to navigate to where symbols are defined
- Use `lsp_call_hierarchy` to understand what calls what

3. **Code Modification**:
3. **Before Code Modification**:
- Use `lsp_references` to find ALL usages of any symbol you're about to modify
- Use `lsp_implementations` before modifying interfaces
- Read files containing references to understand the full impact

4. **Code Modification**:
- Make necessary code changes
- Use `lsp_rename` for safe cross-codebase renames
- Ensure changes follow best practices
- Maintain code style consistency

4. **Validation Loop**:
5. **Validation Loop**:
- Run `lsp_diagnostics` on EVERY file you modified - this catches errors immediately
- Use `lsp_code_actions` to get quick fixes for any errors
- Run `lsp_format` to ensure consistent formatting
- Run linters and tests to check code quality
- Verify changes meet requirements
- If issues found, return to step 3
- If issues found, return to step 4
- Continue until all requirements are met

5. **Summary**:
6. **Summary**:
- Very concisely summarize the changes made (not in a file)
- For trivial tasks, answer the question without extra information
</TASK>

**Details:**
- As much as possible, use LSP tools for Go code - they provide accurate, real-time compiler information
- Be thorough in code examination before making changes
- Always validate changes before considering the task complete
- Follow Go best practices
Expand All @@ -59,9 +70,8 @@ agents:
- type: filesystem
- type: shell
- type: todo
- type: mcp
- type: lsp
command: gopls
args: ["mcp"]
- type: mcp
ref: docker:ast-grep
config:
Expand Down
5 changes: 1 addition & 4 deletions pkg/cli/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,7 @@ func createUserMessageWithAttachment(userContent, attachmentPath string) *sessio
}

// Ensure we have some text content when attaching a file
textContent := userContent
if strings.TrimSpace(textContent) == "" {
textContent = "Please analyze this attached file."
}
textContent := cmp.Or(strings.TrimSpace(userContent), "Please analyze this attached file.")

// Create message with multi-content including text and image
multiContent := []chat.MessagePart{
Expand Down
5 changes: 2 additions & 3 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"cmp"
"context"
"fmt"
"log/slog"
Expand Down Expand Up @@ -28,9 +29,7 @@ func Load(ctx context.Context, source Reader) (*latest.Config, error) {
if err := yaml.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("looking for version in config file\n%s", yaml.FormatError(err, true, true))
}
if raw.Version == "" {
raw.Version = latest.Version
}
raw.Version = cmp.Or(raw.Version, latest.Version)

oldConfig, err := parseCurrentVersion(data, raw.Version)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/latest/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ type Toolset struct {
Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"`

// For `shell`, `script` or `mcp` tools
// For `shell`, `script`, `mcp` or `lsp` tools
Env map[string]string `json:"env,omitempty"`

// For the `todo` tool
Expand Down
16 changes: 10 additions & 6 deletions pkg/config/latest/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ func (t *Toolset) validate() error {
if t.IgnoreVCS != nil && t.Type != "filesystem" {
return errors.New("ignore_vcs can only be used with type 'filesystem'")
}
if len(t.Env) > 0 && (t.Type != "shell" && t.Type != "script" && t.Type != "mcp") {
return errors.New("env can only be used with type 'shell', 'script' or 'mcp'")
if len(t.Env) > 0 && (t.Type != "shell" && t.Type != "script" && t.Type != "mcp" && t.Type != "lsp") {
return errors.New("env can only be used with type 'shell', 'script', 'mcp' or 'lsp'")
}
if t.Shared && t.Type != "todo" {
return errors.New("shared can only be used with type 'todo'")
}
if t.Command != "" && t.Type != "mcp" {
return errors.New("command can only be used with type 'mcp'")
if t.Command != "" && t.Type != "mcp" && t.Type != "lsp" {
return errors.New("command can only be used with type 'mcp' or 'lsp'")
}
if len(t.Args) > 0 && t.Type != "mcp" {
return errors.New("args can only be used with type 'mcp'")
if len(t.Args) > 0 && t.Type != "mcp" && t.Type != "lsp" {
return errors.New("args can only be used with type 'mcp' or 'lsp'")
}
if t.Ref != "" && t.Type != "mcp" {
return errors.New("ref can only be used with type 'mcp'")
Expand Down Expand Up @@ -103,6 +103,10 @@ func (t *Toolset) validate() error {
if t.URL == "" {
return errors.New("a2a toolset requires a url to be set")
}
case "lsp":
if t.Command == "" {
return errors.New("lsp toolset requires a command to be set")
}
}

return nil
Expand Down
90 changes: 90 additions & 0 deletions pkg/config/latest/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package latest

import (
"testing"

"github.com/goccy/go-yaml"
"github.com/stretchr/testify/require"
)

func TestToolset_Validate_LSP(t *testing.T) {
t.Parallel()

tests := []struct {
name string
config string
wantErr string
}{
{
name: "valid lsp with command",
config: `
version: "3"
agents:
root:
model: "openai/gpt-4"
toolsets:
- type: lsp
command: gopls
`,
wantErr: "",
},
{
name: "lsp missing command",
config: `
version: "3"
agents:
root:
model: "openai/gpt-4"
toolsets:
- type: lsp
`,
wantErr: "lsp toolset requires a command to be set",
},
{
name: "lsp with args",
config: `
version: "3"
agents:
root:
model: "openai/gpt-4"
toolsets:
- type: lsp
command: gopls
args:
- -remote=auto
`,
wantErr: "",
},
{
name: "lsp with env",
config: `
version: "3"
agents:
root:
model: "openai/gpt-4"
toolsets:
- type: lsp
command: gopls
env:
GOFLAGS: "-mod=vendor"
`,
wantErr: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

var cfg Config
err := yaml.Unmarshal([]byte(tt.config), &cfg)

if tt.wantErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErr)
} else {
require.NoError(t, err)
}
})
}
}
6 changes: 6 additions & 0 deletions pkg/config/testdata/invalid_lsp_missing_command.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: "3"
agents:
root:
model: "openai/gpt-4"
toolsets:
- type: lsp
7 changes: 7 additions & 0 deletions pkg/config/testdata/valid_lsp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: "3"
agents:
root:
model: "openai/gpt-4"
toolsets:
- type: lsp
command: gopls
16 changes: 10 additions & 6 deletions pkg/config/v2/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ func (t *Toolset) validate() error {
if t.IgnoreVCS != nil && t.Type != "filesystem" {
return errors.New("ignore_vcs can only be used with type 'filesystem'")
}
if len(t.Env) > 0 && (t.Type != "shell" && t.Type != "script" && t.Type != "mcp") {
return errors.New("env can only be used with type 'shell', 'script' or 'mcp'")
if len(t.Env) > 0 && (t.Type != "shell" && t.Type != "script" && t.Type != "mcp" && t.Type != "lsp") {
return errors.New("env can only be used with type 'shell', 'script', 'mcp' or 'lsp'")
}
if t.Shared && t.Type != "todo" {
return errors.New("shared can only be used with type 'todo'")
}
if t.Command != "" && t.Type != "mcp" {
return errors.New("command can only be used with type 'mcp'")
if t.Command != "" && t.Type != "mcp" && t.Type != "lsp" {
return errors.New("command can only be used with type 'mcp' or 'lsp'")
}
if len(t.Args) > 0 && t.Type != "mcp" {
return errors.New("args can only be used with type 'mcp'")
if len(t.Args) > 0 && t.Type != "mcp" && t.Type != "lsp" {
return errors.New("args can only be used with type 'mcp' or 'lsp'")
}
if t.Ref != "" && t.Type != "mcp" {
return errors.New("ref can only be used with type 'mcp'")
Expand Down Expand Up @@ -90,6 +90,10 @@ func (t *Toolset) validate() error {
if t.Ref != "" && !strings.Contains(t.Ref, "docker:") {
return errors.New("only docker refs are supported for MCP tools, e.g., 'docker:context7'")
}
case "lsp":
if t.Command == "" {
return errors.New("lsp toolset requires a command to be set")
}
}

return nil
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func TestValidationErrors(t *testing.T) {
name: "skills enabled without read_file tool",
path: "skills_missing_read_file.yaml",
},
{
name: "lsp toolset missing command",
path: "invalid_lsp_missing_command.yaml",
},
}

for _, tt := range tests {
Expand Down
3 changes: 1 addition & 2 deletions pkg/history/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ func New(opts ...Opt) (*History, error) {
homeDir := o.homeDir
if homeDir == "" {
var err error
homeDir, err = os.UserHomeDir()
if err != nil {
if homeDir, err = os.UserHomeDir(); err != nil {
return nil, err
}
}
Expand Down
7 changes: 2 additions & 5 deletions pkg/rag/fusion/fusion.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fusion

import (
"cmp"
"fmt"

"github.com/docker/cagent/pkg/rag/database"
Expand All @@ -24,11 +25,7 @@ type Config struct {
func New(config Config) (Fusion, error) {
switch config.Strategy {
case "rrf", "reciprocal_rank_fusion", "":
k := config.K
if k == 0 {
k = 60 // Standard RRF parameter
}
return NewReciprocalRankFusion(k), nil
return NewReciprocalRankFusion(cmp.Or(config.K, 60)), nil

case "weighted":
if len(config.Weights) == 0 {
Expand Down
10 changes: 10 additions & 0 deletions pkg/teamloader/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func NewDefaultToolsetRegistry() *ToolsetRegistry {
r.Register("mcp", createMCPTool)
r.Register("api", createAPITool)
r.Register("a2a", createA2ATool)
r.Register("lsp", createLSPTool)
return r
}

Expand Down Expand Up @@ -244,3 +245,12 @@ func createA2ATool(ctx context.Context, toolset latest.Toolset, _ string, runCon

return a2a.NewToolset(toolset.Name, toolset.URL, headers), nil
}

func createLSPTool(ctx context.Context, toolset latest.Toolset, _ string, runConfig *config.RuntimeConfig) (tools.ToolSet, error) {
env, err := environment.ExpandAll(ctx, environment.ToValues(toolset.Env), runConfig.EnvProvider())
if err != nil {
return nil, fmt.Errorf("failed to expand the tool's environment variables: %w", err)
}
env = append(env, os.Environ()...)
return builtin.NewLSPTool(toolset.Command, toolset.Args, env, runConfig.WorkingDir), nil
}
Loading