Merge branch 'fix/io-migration-agentic' into new

# Conflicts:
#	pkg/agentic/config.go
#	pkg/agentic/context.go
This commit is contained in:
Snider 2026-02-08 21:28:09 +00:00
commit 0a553dcf6e
2 changed files with 70 additions and 114 deletions

View file

@ -1,26 +1,25 @@
package agentic
import (
"bufio"
"os"
"path/filepath"
"strings"
"github.com/host-uk/core/pkg/config"
"github.com/host-uk/core/pkg/errors"
"github.com/host-uk/core/pkg/io"
"github.com/host-uk/core/pkg/log"
"gopkg.in/yaml.v3"
)
// Config holds the configuration for connecting to the core-agentic service.
type Config struct {
// BaseURL is the URL of the core-agentic API server.
BaseURL string `yaml:"base_url" json:"base_url" mapstructure:"base_url"`
BaseURL string `yaml:"base_url" json:"base_url"`
// Token is the authentication token for API requests.
Token string `yaml:"token" json:"token" mapstructure:"token"`
Token string `yaml:"token" json:"token"`
// DefaultProject is the project to use when none is specified.
DefaultProject string `yaml:"default_project" json:"default_project" mapstructure:"default_project"`
DefaultProject string `yaml:"default_project" json:"default_project"`
// AgentID is the identifier for this agent (optional, used for claiming tasks).
AgentID string `yaml:"agent_id" json:"agent_id" mapstructure:"agent_id"`
AgentID string `yaml:"agent_id" json:"agent_id"`
}
// configFileName is the name of the YAML config file.
@ -33,9 +32,10 @@ const envFileName = ".env"
const DefaultBaseURL = "https://api.core-agentic.dev"
// LoadConfig loads the agentic configuration from the specified directory.
// It uses the centralized config service.
// It first checks for a .env file, then falls back to ~/.core/agentic.yaml.
// If dir is empty, it checks the current directory first.
//
// Environment variables take precedence (prefix: AGENTIC_):
// Environment variables take precedence:
// - AGENTIC_BASE_URL: API base URL
// - AGENTIC_TOKEN: Authentication token
// - AGENTIC_PROJECT: Default project
@ -58,13 +58,15 @@ func LoadConfig(dir string) (*Config, error) {
}
// Try loading from current directory .env
cwd, err := os.Getwd()
if err == nil {
envPath := filepath.Join(cwd, envFileName)
if err := loadEnvFile(envPath, cfg); err == nil {
applyEnvOverrides(cfg)
if cfg.Token != "" {
return cfg, nil
if dir == "" {
cwd, err := os.Getwd()
if err == nil {
envPath := filepath.Join(cwd, envFileName)
if err := loadEnvFile(envPath, cfg); err == nil {
applyEnvOverrides(cfg)
if cfg.Token != "" {
return cfg, nil
}
}
}
}
@ -72,19 +74,12 @@ func LoadConfig(dir string) (*Config, error) {
// Try loading from ~/.core/agentic.yaml
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, log.E("agentic.LoadConfig", "failed to get home directory", err)
return nil, errors.E("agentic.LoadConfig", "failed to get home directory", err)
}
configPath := filepath.Join(homeDir, ".core", configFileName)
if io.Local.IsFile(configPath) {
// Use centralized config service to load the YAML file
c, err := config.New(config.WithPath(configPath))
if err != nil {
return nil, log.E("agentic.LoadConfig", "failed to initialize config", err)
}
if err := c.Get("", cfg); err != nil {
return nil, log.E("agentic.LoadConfig", "failed to load config", err)
}
if err := loadYAMLConfig(configPath, cfg); err != nil && !os.IsNotExist(err) {
return nil, errors.E("agentic.LoadConfig", "failed to load config", err)
}
// Apply environment variable overrides
@ -92,25 +87,21 @@ func LoadConfig(dir string) (*Config, error) {
// Validate configuration
if cfg.Token == "" {
log.Security("agentic authentication failed: no token configured", "user", log.Username())
return nil, log.E("agentic.LoadConfig", "no authentication token configured", nil)
return nil, errors.E("agentic.LoadConfig", "no authentication token configured", nil)
}
log.Security("agentic configuration loaded", "user", log.Username(), "baseURL", cfg.BaseURL)
return cfg, nil
}
// loadEnvFile reads a .env file and extracts agentic configuration.
func loadEnvFile(path string, cfg *Config) error {
file, err := os.Open(path)
content, err := io.Local.Read(path)
if err != nil {
return err
}
defer func() { _ = file.Close() }()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
for _, line := range strings.Split(content, "\n") {
line = strings.TrimSpace(line)
// Skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
@ -141,7 +132,17 @@ func loadEnvFile(path string, cfg *Config) error {
}
}
return scanner.Err()
return nil
}
// loadYAMLConfig reads configuration from a YAML file.
func loadYAMLConfig(path string, cfg *Config) error {
content, err := io.Local.Read(path)
if err != nil {
return err
}
return yaml.Unmarshal([]byte(content), cfg)
}
// applyEnvOverrides applies environment variable overrides to the config.
@ -162,25 +163,35 @@ func applyEnvOverrides(cfg *Config) {
// SaveConfig saves the configuration to ~/.core/agentic.yaml.
func SaveConfig(cfg *Config) error {
path, err := ConfigPath()
homeDir, err := os.UserHomeDir()
if err != nil {
return err
return errors.E("agentic.SaveConfig", "failed to get home directory", err)
}
data := make(map[string]any)
data["base_url"] = cfg.BaseURL
data["token"] = cfg.Token
data["default_project"] = cfg.DefaultProject
data["agent_id"] = cfg.AgentID
configDir := filepath.Join(homeDir, ".core")
if err := io.Local.EnsureDir(configDir); err != nil {
return errors.E("agentic.SaveConfig", "failed to create config directory", err)
}
return config.Save(io.Local, path, data)
configPath := filepath.Join(configDir, configFileName)
data, err := yaml.Marshal(cfg)
if err != nil {
return errors.E("agentic.SaveConfig", "failed to marshal config", err)
}
if err := io.Local.Write(configPath, string(data)); err != nil {
return errors.E("agentic.SaveConfig", "failed to write config file", err)
}
return nil
}
// ConfigPath returns the path to the config file in the user's home directory.
func ConfigPath() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", log.E("agentic.ConfigPath", "failed to get home directory", err)
return "", errors.E("agentic.ConfigPath", "failed to get home directory", err)
}
return filepath.Join(homeDir, ".core", configFileName), nil
}

View file

@ -3,20 +3,16 @@ package agentic
import (
"bytes"
goio "io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/host-uk/core/pkg/ai"
"github.com/host-uk/core/pkg/errors"
"github.com/host-uk/core/pkg/io"
"github.com/host-uk/core/pkg/log"
)
const maxContextBytes = 5000
// FileContent represents the content of a file for AI context.
type FileContent struct {
// Path is the relative path to the file.
@ -39,8 +35,6 @@ type TaskContext struct {
RecentCommits string `json:"recent_commits"`
// RelatedCode contains code snippets related to the task.
RelatedCode []FileContent `json:"related_code"`
// RAGContext contains relevant documentation from the vector database.
RAGContext string `json:"rag_context,omitempty"`
}
// BuildTaskContext gathers context for AI collaboration on a task.
@ -48,13 +42,13 @@ func BuildTaskContext(task *Task, dir string) (*TaskContext, error) {
const op = "agentic.BuildTaskContext"
if task == nil {
return nil, log.E(op, "task is required", nil)
return nil, errors.E(op, "task is required", nil)
}
if dir == "" {
cwd, err := os.Getwd()
if err != nil {
return nil, log.E(op, "failed to get working directory", err)
return nil, errors.E(op, "failed to get working directory", err)
}
dir = cwd
}
@ -86,13 +80,6 @@ func BuildTaskContext(task *Task, dir string) (*TaskContext, error) {
}
ctx.RelatedCode = relatedCode
// Query RAG for relevant documentation (graceful degradation)
ragCtx := ai.QueryRAGForTask(ai.TaskInfo{
Title: task.Title,
Description: task.Description,
})
ctx.RAGContext = ragCtx
return ctx, nil
}
@ -101,31 +88,24 @@ func GatherRelatedFiles(task *Task, dir string) ([]FileContent, error) {
const op = "agentic.GatherRelatedFiles"
if task == nil {
return nil, log.E(op, "task is required", nil)
return nil, errors.E(op, "task is required", nil)
}
var files []FileContent
// Read files explicitly mentioned in the task
for _, relPath := range task.Files {
fullPath := relPath
if !filepath.IsAbs(relPath) {
fullPath = filepath.Join(dir, relPath)
}
fullPath := filepath.Join(dir, relPath)
content, truncated, err := readAndTruncate(fullPath)
content, err := io.Local.Read(fullPath)
if err != nil {
// Skip files that don't exist
continue
}
contentStr := string(content)
if truncated {
contentStr += "\n... (truncated)"
}
files = append(files, FileContent{
Path: relPath,
Content: contentStr,
Content: content,
Language: detectLanguage(relPath),
})
}
@ -138,7 +118,7 @@ func findRelatedCode(task *Task, dir string) ([]FileContent, error) {
const op = "agentic.findRelatedCode"
if task == nil {
return nil, log.E(op, "task is required", nil)
return nil, errors.E(op, "task is required", nil)
}
// Extract keywords from title and description
@ -174,24 +154,20 @@ func findRelatedCode(task *Task, dir string) ([]FileContent, error) {
break
}
fullPath := line
if !filepath.IsAbs(line) {
fullPath = filepath.Join(dir, line)
}
content, truncated, err := readAndTruncate(fullPath)
fullPath := filepath.Join(dir, line)
content, err := io.Local.Read(fullPath)
if err != nil {
continue
}
contentStr := string(content)
if truncated {
contentStr += "\n... (truncated)"
// Truncate large files
if len(content) > 5000 {
content = content[:5000] + "\n... (truncated)"
}
files = append(files, FileContent{
Path: line,
Content: contentStr,
Content: content,
Language: detectLanguage(line),
})
}
@ -286,30 +262,6 @@ func detectLanguage(path string) string {
return "text"
}
// readAndTruncate reads up to maxContextBytes from a file.
func readAndTruncate(path string) ([]byte, bool, error) {
f, err := io.Local.ReadStream(path)
if err != nil {
return nil, false, err
}
defer func() { _ = f.Close() }()
// Read up to maxContextBytes + 1 to detect truncation
reader := goio.LimitReader(f, maxContextBytes+1)
content, err := goio.ReadAll(reader)
if err != nil {
return nil, false, err
}
truncated := false
if len(content) > maxContextBytes {
content = content[:maxContextBytes]
truncated = true
}
return content, truncated, nil
}
// runGitCommand runs a git command and returns the output.
func runGitCommand(dir string, args ...string) (string, error) {
cmd := exec.Command("git", args...)
@ -379,12 +331,5 @@ func (tc *TaskContext) FormatContext() string {
}
}
// Relevant documentation from RAG
if tc.RAGContext != "" {
sb.WriteString("## Relevant Documentation\n")
sb.WriteString(tc.RAGContext)
sb.WriteString("\n\n")
}
return sb.String()
}