Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f7e569826 |
@ -82,13 +82,22 @@ func (a *Agent) Run(ctx context.Context, out Output) {
|
|||||||
}
|
}
|
||||||
if retryCount < maxRetries && isRetryableError(err) {
|
if retryCount < maxRetries && isRetryableError(err) {
|
||||||
retryCount++
|
retryCount++
|
||||||
out.Error(fmt.Sprintf("LLM produced malformed output, retrying (%d/%d)...", retryCount, maxRetries))
|
if isConnectionError(err) {
|
||||||
|
out.Error(fmt.Sprintf("Connection lost, retrying (%d/%d) in 2s...", retryCount, maxRetries))
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Error(fmt.Sprintf("LLM produced malformed output, retrying (%d/%d)...", retryCount, maxRetries))
|
||||||
|
}
|
||||||
textBuf.Reset()
|
textBuf.Reset()
|
||||||
toolCalls = nil
|
toolCalls = nil
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out.Error(fmt.Sprintf("LLM error: %v", err))
|
out.Error(fmt.Sprintf("LLM error: %v", err))
|
||||||
out.SystemMessage(fmt.Sprintf("⚠️ Model response failed: %v\n\nYou can try:\n- Checking if Ollama is running (`ollama ps`)\n- Switching to a different model (ctrl+m)\n- Reducing context size\n\nTool results are still available above.", err))
|
out.SystemMessage(fmt.Sprintf("⚠️ Model response failed: %v\n\nYou can try:\n- Checking if Ollama is running (`ollama ps`)\n- Switching to a different model (F6)\n- Reducing context size (num_ctx in config)\n\nTool results are still available above.", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
retryCount = 0
|
retryCount = 0
|
||||||
@ -240,7 +249,19 @@ func (a *Agent) Run(ctx context.Context, out Output) {
|
|||||||
|
|
||||||
func isRetryableError(err error) bool {
|
func isRetryableError(err error) bool {
|
||||||
msg := err.Error()
|
msg := err.Error()
|
||||||
return strings.Contains(msg, "parse JSON") || strings.Contains(msg, "unexpected end of JSON")
|
if strings.Contains(msg, "parse JSON") || strings.Contains(msg, "unexpected end of JSON") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return isConnectionError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isConnectionError reports transient connection failures (EOF, reset, refused) that may succeed on retry.
|
||||||
|
func isConnectionError(err error) bool {
|
||||||
|
msg := err.Error()
|
||||||
|
return strings.Contains(msg, "EOF") ||
|
||||||
|
strings.Contains(msg, "connection reset") ||
|
||||||
|
strings.Contains(msg, "connection refused") ||
|
||||||
|
strings.Contains(msg, "broken pipe")
|
||||||
}
|
}
|
||||||
|
|
||||||
func FormatToolArgs(args map[string]any) string {
|
func FormatToolArgs(args map[string]any) string {
|
||||||
|
|||||||
372
main.go
372
main.go
@ -1,372 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"ai-agent/internal/agent"
|
|
||||||
"ai-agent/internal/command"
|
|
||||||
"ai-agent/internal/config"
|
|
||||||
"ai-agent/internal/db"
|
|
||||||
"ai-agent/internal/ice"
|
|
||||||
"ai-agent/internal/initcmd"
|
|
||||||
"ai-agent/internal/llm"
|
|
||||||
"ai-agent/internal/logging"
|
|
||||||
"ai-agent/internal/mcp"
|
|
||||||
"ai-agent/internal/memory"
|
|
||||||
"ai-agent/internal/permission"
|
|
||||||
"ai-agent/internal/skill"
|
|
||||||
"ai-agent/internal/tui"
|
|
||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var version = "mydev"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
for _, arg := range os.Args[1:] {
|
|
||||||
if arg == "--version" || arg == "-version" {
|
|
||||||
fmt.Println(version)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(os.Args) > 1 {
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "init":
|
|
||||||
force := false
|
|
||||||
for _, arg := range os.Args[2:] {
|
|
||||||
if arg == "--force" || arg == "-force" {
|
|
||||||
force = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := initcmd.Run(".", initcmd.Options{Force: force}); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "init: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Println("AGENT.md created successfully.")
|
|
||||||
return
|
|
||||||
case "logs":
|
|
||||||
handleLogs(os.Args[2:])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
qwenRouterFlag := flag.Bool("qwen-router", false, "use optimized Qwen model router (experimental)")
|
|
||||||
modelFlag := flag.String("model", "", "override Ollama model")
|
|
||||||
agentProfileFlag := flag.String("agent", "", "override agent profile")
|
|
||||||
promptFlag := flag.String("p", "", "run in non-interactive mode: send prompt, print response, exit")
|
|
||||||
yoloFlag := flag.Bool("yolo", false, "auto-approve all tool calls (skip permission prompts)")
|
|
||||||
flag.Parse()
|
|
||||||
cfg, agentsDir, err := config.LoadWithAgentsDir()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("config: %v", err)
|
|
||||||
}
|
|
||||||
if *modelFlag != "" {
|
|
||||||
cfg.Ollama.Model = *modelFlag
|
|
||||||
}
|
|
||||||
if *agentProfileFlag != "" {
|
|
||||||
cfg.AgentProfile = *agentProfileFlag
|
|
||||||
}
|
|
||||||
var router *config.Router
|
|
||||||
if *qwenRouterFlag {
|
|
||||||
fmt.Fprintf(os.Stderr, "Using Qwen-optimized model router (experimental)\n")
|
|
||||||
}
|
|
||||||
router = config.NewRouter(&cfg.Model)
|
|
||||||
modelName := cfg.Ollama.Model
|
|
||||||
if cfg.AgentProfile != "" && agentsDir != nil {
|
|
||||||
if profile := agentsDir.GetAgent(cfg.AgentProfile); profile != nil {
|
|
||||||
if profile.Model != "" {
|
|
||||||
modelName = profile.Model
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
modelManager := llm.NewModelManager(cfg.Ollama.BaseURL, cfg.Ollama.NumCtx)
|
|
||||||
modelManager.SetCurrentModel(modelName)
|
|
||||||
var servers []config.ServerConfig
|
|
||||||
if len(cfg.Servers) > 0 {
|
|
||||||
servers = cfg.Servers
|
|
||||||
} else if agentsDir != nil && agentsDir.HasMCP() {
|
|
||||||
servers = agentsDir.GetMCPServers()
|
|
||||||
}
|
|
||||||
registry := mcp.NewRegistry()
|
|
||||||
defer registry.Close()
|
|
||||||
ag := agent.New(modelManager, registry, cfg.Ollama.NumCtx)
|
|
||||||
ag.SetToolsConfig(cfg.Tools)
|
|
||||||
ag.SetRouter(router)
|
|
||||||
if wd, err := os.Getwd(); err == nil {
|
|
||||||
ag.SetWorkDir(wd)
|
|
||||||
}
|
|
||||||
defer ag.Close()
|
|
||||||
dbStore, err := db.Open()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("warning: database: %v (permissions disabled)", err)
|
|
||||||
}
|
|
||||||
if dbStore != nil {
|
|
||||||
defer dbStore.Close()
|
|
||||||
}
|
|
||||||
permChecker := permission.NewChecker(dbStore, *yoloFlag)
|
|
||||||
ag.SetPermissionChecker(permChecker)
|
|
||||||
memStore := memory.NewStore("")
|
|
||||||
ag.SetMemoryStore(memStore)
|
|
||||||
skillDirs := []string{cfg.SkillsDir}
|
|
||||||
if agentsDir != nil && len(agentsDir.Skills) > 0 {
|
|
||||||
for _, s := range agentsDir.Skills {
|
|
||||||
if s.Path != "" {
|
|
||||||
skillDir := filepath.Dir(s.Path)
|
|
||||||
if skillDir != "" {
|
|
||||||
skillDirs = append(skillDirs, skillDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
skillMgr := skill.NewManager("")
|
|
||||||
for _, dir := range skillDirs {
|
|
||||||
if dir != "" {
|
|
||||||
skillMgr.AddSearchPath(dir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = skillMgr.LoadAll()
|
|
||||||
if *promptFlag != "" {
|
|
||||||
ctx := context.Background()
|
|
||||||
fmt.Fprintf(os.Stderr, "connecting to Ollama (%s)...\n", modelName)
|
|
||||||
if err := modelManager.Ping(); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "ollama: %v\nhint: is `ollama serve` running? is %q pulled?\n", err, modelName)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for _, srv := range servers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(s config.ServerConfig) {
|
|
||||||
defer wg.Done()
|
|
||||||
fmt.Fprintf(os.Stderr, "connecting MCP server %s...\n", s.Name)
|
|
||||||
if _, err := registry.ConnectServer(ctx, s); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "MCP server %s failed: %v\n", s.Name, err)
|
|
||||||
}
|
|
||||||
}(srv)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
if cfg.ICE.Enabled {
|
|
||||||
embedModel := cfg.ICE.EmbedModel
|
|
||||||
if embedModel == "" {
|
|
||||||
embedModel = cfg.Model.EmbedModel
|
|
||||||
}
|
|
||||||
iceEngine, err := ice.NewEngine(modelManager, memStore, ice.EngineConfig{
|
|
||||||
EmbedModel: embedModel,
|
|
||||||
StorePath: cfg.ICE.StorePath,
|
|
||||||
NumCtx: cfg.Ollama.NumCtx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "ICE: %v\n", err)
|
|
||||||
} else {
|
|
||||||
ag.SetICEEngine(iceEngine)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if agentsDir != nil && agentsDir.GetGlobalInstructions() != "" {
|
|
||||||
ag.SetLoadedContext(agentsDir.GetGlobalInstructions())
|
|
||||||
}
|
|
||||||
if data, err := os.ReadFile("AGENT.md"); err == nil {
|
|
||||||
ag.AppendLoadedContext("\n\n" + string(data))
|
|
||||||
}
|
|
||||||
if cfg.AgentProfile != "" && agentsDir != nil {
|
|
||||||
if profile := agentsDir.GetAgent(cfg.AgentProfile); profile != nil {
|
|
||||||
if profile.SystemPrompt != "" {
|
|
||||||
ag.AppendLoadedContext("\n\n" + profile.SystemPrompt)
|
|
||||||
}
|
|
||||||
for _, skillName := range profile.Skills {
|
|
||||||
skillMgr.Activate(skillName)
|
|
||||||
}
|
|
||||||
ag.SetSkillContent(skillMgr.ActiveContent())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
modes := tui.DefaultModeConfigs()
|
|
||||||
buildMode := modes[tui.ModeBuild]
|
|
||||||
ag.SetModeContext(buildMode.SystemPromptPrefix, buildMode.AllowTools)
|
|
||||||
out := agent.NewHeadlessOutput()
|
|
||||||
ag.AddUserMessage(*promptFlag)
|
|
||||||
ag.Run(ctx, out)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cmdReg := command.NewRegistry()
|
|
||||||
command.RegisterBuiltins(cmdReg)
|
|
||||||
if home, err := os.UserHomeDir(); err == nil {
|
|
||||||
customDir := filepath.Join(home, ".config", "ai-agent", "commands")
|
|
||||||
command.RegisterCustomCommands(cmdReg, customDir)
|
|
||||||
}
|
|
||||||
modelList := []string{modelName}
|
|
||||||
var agentList []string
|
|
||||||
if agentsDir != nil {
|
|
||||||
for _, a := range agentsDir.ListAgents() {
|
|
||||||
agentList = append(agentList, a.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completer := tui.NewCompleter(cmdReg, modelList, skillMgr.Names(), agentList, registry)
|
|
||||||
logger, logFile, err := logging.NewSessionLogger()
|
|
||||||
if err != nil {
|
|
||||||
// Non-fatal; logging disabled.
|
|
||||||
}
|
|
||||||
if logFile != nil {
|
|
||||||
defer logFile.Close()
|
|
||||||
}
|
|
||||||
m := tui.New(ag, cmdReg, skillMgr, completer, modelManager, router, logger)
|
|
||||||
p := tea.NewProgram(m)
|
|
||||||
m.SetProgram(p)
|
|
||||||
initCtx, initCancel := context.WithCancel(context.Background())
|
|
||||||
m.SetInitCancel(initCancel)
|
|
||||||
initDone := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(initDone)
|
|
||||||
p.Send(tui.StartupStatusMsg{ID: "ollama", Label: "Ollama (" + modelName + ")", Status: "connecting"})
|
|
||||||
if err := modelManager.Ping(); err != nil {
|
|
||||||
p.Send(tui.StartupStatusMsg{ID: "ollama", Label: "Ollama (" + modelName + ")", Status: "failed", Detail: err.Error()})
|
|
||||||
p.Send(tui.ErrorMsg{Msg: fmt.Sprintf("ollama: %v\nhint: is `ollama serve` running? is %q pulled?", err, modelName)})
|
|
||||||
} else {
|
|
||||||
p.Send(tui.StartupStatusMsg{ID: "ollama", Label: "Ollama (" + modelName + ")", Status: "connected"})
|
|
||||||
}
|
|
||||||
if initCtx.Err() != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if list, err := modelManager.ListModels(initCtx); err == nil && len(list) > 0 {
|
|
||||||
modelList = list
|
|
||||||
}
|
|
||||||
if initCtx.Err() != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for _, srv := range servers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(s config.ServerConfig) {
|
|
||||||
defer wg.Done()
|
|
||||||
p.Send(tui.StartupStatusMsg{ID: "mcp:" + s.Name, Label: s.Name, Status: "connecting"})
|
|
||||||
if initCtx.Err() != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toolCount, err := registry.ConnectServer(initCtx, s)
|
|
||||||
if err != nil {
|
|
||||||
p.Send(tui.StartupStatusMsg{ID: "mcp:" + s.Name, Label: s.Name, Status: "failed", Detail: err.Error()})
|
|
||||||
} else {
|
|
||||||
p.Send(tui.StartupStatusMsg{ID: "mcp:" + s.Name, Label: s.Name, Status: "connected", Detail: fmt.Sprintf("%d tools", toolCount)})
|
|
||||||
}
|
|
||||||
}(srv)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
if initCtx.Err() != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var iceEnabled bool
|
|
||||||
var iceConversations int
|
|
||||||
var iceSessionID string
|
|
||||||
if cfg.ICE.Enabled {
|
|
||||||
p.Send(tui.StartupStatusMsg{ID: "ice", Label: "ICE", Status: "connecting"})
|
|
||||||
embedModel := cfg.ICE.EmbedModel
|
|
||||||
if embedModel == "" {
|
|
||||||
embedModel = cfg.Model.EmbedModel
|
|
||||||
}
|
|
||||||
iceEngine, err := ice.NewEngine(modelManager, memStore, ice.EngineConfig{
|
|
||||||
EmbedModel: embedModel,
|
|
||||||
StorePath: cfg.ICE.StorePath,
|
|
||||||
NumCtx: cfg.Ollama.NumCtx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
p.Send(tui.StartupStatusMsg{ID: "ice", Label: "ICE", Status: "failed", Detail: err.Error()})
|
|
||||||
} else {
|
|
||||||
ag.SetICEEngine(iceEngine)
|
|
||||||
iceEnabled = true
|
|
||||||
iceConversations = iceEngine.Store().Count()
|
|
||||||
iceSessionID = iceEngine.SessionID()
|
|
||||||
p.Send(tui.StartupStatusMsg{ID: "ice", Label: "ICE", Status: "connected", Detail: fmt.Sprintf("%d conversations", iceConversations)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if agentsDir != nil && agentsDir.GetGlobalInstructions() != "" {
|
|
||||||
ag.SetLoadedContext(agentsDir.GetGlobalInstructions())
|
|
||||||
}
|
|
||||||
if data, err := os.ReadFile("AGENT.md"); err == nil {
|
|
||||||
ag.AppendLoadedContext("\n\n" + string(data))
|
|
||||||
}
|
|
||||||
if cfg.AgentProfile != "" && agentsDir != nil {
|
|
||||||
if profile := agentsDir.GetAgent(cfg.AgentProfile); profile != nil {
|
|
||||||
if profile.SystemPrompt != "" {
|
|
||||||
ag.AppendLoadedContext("\n\n" + profile.SystemPrompt)
|
|
||||||
}
|
|
||||||
for _, skillName := range profile.Skills {
|
|
||||||
skillMgr.Activate(skillName)
|
|
||||||
}
|
|
||||||
ag.SetSkillContent(skillMgr.ActiveContent())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var failedServers []tui.FailedServer
|
|
||||||
for _, fs := range registry.FailedServers() {
|
|
||||||
failedServers = append(failedServers, tui.FailedServer{
|
|
||||||
Name: fs.Name,
|
|
||||||
Reason: fs.Reason,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p.Send(tui.InitCompleteMsg{
|
|
||||||
Model: modelName,
|
|
||||||
ModelList: modelList,
|
|
||||||
AgentProfile: cfg.AgentProfile,
|
|
||||||
AgentList: agentList,
|
|
||||||
ToolCount: ag.ToolCount(),
|
|
||||||
ServerCount: registry.ServerCount(),
|
|
||||||
NumCtx: cfg.Ollama.NumCtx,
|
|
||||||
FailedServers: failedServers,
|
|
||||||
ICEEnabled: iceEnabled,
|
|
||||||
ICEConversations: iceConversations,
|
|
||||||
ICESessionID: iceSessionID,
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
if _, err := p.Run(); err != nil {
|
|
||||||
log.Fatalf("tui: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
initCancel()
|
|
||||||
<-initDone
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLogs(args []string) {
|
|
||||||
follow := false
|
|
||||||
for _, arg := range args {
|
|
||||||
if arg == "-f" {
|
|
||||||
follow = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if follow {
|
|
||||||
latest, err := logging.LatestLogPath()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "logs: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(os.Stderr, "following %s\n", latest)
|
|
||||||
tailBin, err := exec.LookPath("tail")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "logs: tail not found: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if err := syscall.Exec(tailBin, []string{"tail", "-f", latest}, os.Environ()); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "logs: exec tail: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
entries, err := logging.ListLogs(20)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "logs: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if len(entries) == 0 {
|
|
||||||
fmt.Println("No log files found in", logging.LogDir())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("Recent sessions (%s):\n\n", logging.LogDir())
|
|
||||||
for _, e := range entries {
|
|
||||||
name := filepath.Base(e.Path)
|
|
||||||
sizeKB := float64(e.Size) / 1024
|
|
||||||
fmt.Printf(" %-30s %s %6.1f KB\n", name, e.ModTime.Format("2006-01-02 15:04:05"), sizeKB)
|
|
||||||
}
|
|
||||||
fmt.Printf("\nTip: run `ai-agent logs -f` to follow the latest log.\n")
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user