From e18dde7c150c5781800114a457982239478ef4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E6=A3=AE?= Date: Sun, 10 May 2026 14:28:17 +0800 Subject: [PATCH] feat: implement TUI with bubbletea and multi-agent collaboration - Add bubbletea/lipgloss/glamour dependencies for TUI - Create internal/tui package with EventWriter, styles, and bubbletea Model - Support streaming output display in conversation window - Add right panel with statistics and active agent status - Implement multi-agent collaboration with sub-agents - Add AgentCallTool for delegating tasks to sub-agents - Support parallel tool execution with goroutines - Auto-discover sub-agents from ~/.orca/prompts/ directory - Fix orchestrator routing based on msg.To field - Add non-blocking event writer with timeout to prevent blocking --- .gitignore | 6 + cmd/orca/main.go | 211 +-------------- go.mod | 40 +++ go.sum | 91 +++++++ internal/config/config.go | 187 ++++++++------ internal/tui/model.go | 330 ++++++++++++++++++++++++ internal/tui/styles.go | 73 ++++++ internal/tui/writer.go | 51 ++++ pkg/actor/llm_agent.go | 285 ++++++++++++++++---- pkg/actor/orchestrator.go | 56 +++- pkg/actor/subagent.go | 149 +++++++++++ pkg/actor/system.go | 37 +-- pkg/actor/worker.go | 21 +- pkg/kernel/kernel.go | 254 +++++++++++------- pkg/plugin/registry.go | 23 +- pkg/sandbox/process.go | 4 +- pkg/session/jsonl.go | 39 +-- pkg/session/manager.go | 30 ++- pkg/session/store.go | 25 +- pkg/skill/manager.go | 40 +-- pkg/skill/parser.go | 29 ++- pkg/skill/skill.go | 39 ++- pkg/tool/agent_call.go | 79 ++++++ pkg/tool/builtin.go | 4 +- pkg/tool/manager.go | 31 ++- thoughts/ledgers/CONTINUITY_ses_1fd1.md | 82 ++++++ 26 files changed, 1647 insertions(+), 569 deletions(-) create mode 100644 .gitignore create mode 100644 go.sum create mode 100644 internal/tui/model.go create mode 100644 internal/tui/styles.go create mode 100644 internal/tui/writer.go create mode 100644 pkg/actor/subagent.go create mode 100644 pkg/tool/agent_call.go create mode 100644 thoughts/ledgers/CONTINUITY_ses_1fd1.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1a948e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +orca +test.pdf +~/ +.memory/ +config.toml.example +prompts/ diff --git a/cmd/orca/main.go b/cmd/orca/main.go index 96e208c..4a70e6c 100644 --- a/cmd/orca/main.go +++ b/cmd/orca/main.go @@ -1,58 +1,23 @@ -// Orca is a Go-based Agent framework with a microkernel architecture. -// -// It supports multi-agent collaboration, persistent session memory, -// skill-based automation, sandboxed execution, custom tool registration, -// and local LLM integration via Ollama. package main import ( - "bufio" "fmt" "log" "os" - "os/signal" - "strings" - "syscall" - "time" + tea "github.com/charmbracelet/bubbletea" "github.com/orca/orca/internal/config" + "github.com/orca/orca/internal/tui" "github.com/orca/orca/pkg/kernel" ) func main() { - // Load configuration from environment variables - cfg := config.LoadConfigFromEnv() - - if v := os.Getenv("OLLAMA_BASE_URL"); v != "" { - cfg.Ollama.BaseURL = v - } - if v := os.Getenv("OLLAMA_MODEL"); v != "" { - cfg.Ollama.Model = v - } - if v := os.Getenv("OLLAMA_TIMEOUT"); v != "" { - if d, err := time.ParseDuration(v); err == nil { - cfg.Ollama.Timeout = d - } + cfg, err := config.LoadConfig() + if err != nil { + log.Fatalf("Failed to load config: %v", err) } - if v := os.Getenv("DEEPSEEK_BASE_URL"); v != "" { - cfg.DeepSeek.BaseURL = v - } - if v := os.Getenv("DEEPSEEK_MODEL"); v != "" { - cfg.DeepSeek.Model = v - } - if v := os.Getenv("DEEPSEEK_API_KEY"); v != "" { - cfg.DeepSeek.APIKey = v - } - if v := os.Getenv("DEEPSEEK_TIMEOUT"); v != "" { - if d, err := time.ParseDuration(v); err == nil { - cfg.DeepSeek.Timeout = d - } - } - - // Create and start kernel k := kernel.NewWithConfig(cfg) - if err := k.Start(); err != nil { log.Fatalf("Failed to start kernel: %v", err) } @@ -61,169 +26,15 @@ func main() { log.Printf("Warning: failed to load skills: %v", err) } - k.SetStreamWriter(os.Stdout) + m := tui.NewModel(k) + p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion()) - fmt.Println("Orca Agent Framework") - fmt.Println("Kernel started successfully") - if cfg.Provider == config.ProviderDeepSeek { - fmt.Printf(" Provider: DeepSeek\n") - fmt.Printf(" LLM Model: %s\n", cfg.DeepSeek.Model) - } else { - fmt.Printf(" Provider: Ollama\n") - fmt.Printf(" LLM Model: %s\n", cfg.Ollama.Model) - fmt.Printf(" Ollama URL: %s\n", cfg.Ollama.BaseURL) - } - fmt.Println("Type your message or /help for commands.") - fmt.Println() - - // Handle graceful shutdown - sig := make(chan os.Signal, 1) - signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) - - // REPL loop in a goroutine so we can catch signals - done := make(chan struct{}) - - go func() { - scanner := bufio.NewScanner(os.Stdin) - for { - fmt.Print("> ") - if !scanner.Scan() { - break - } - - input := strings.TrimSpace(scanner.Text()) - if input == "" { - continue - } - - // Handle commands - if strings.HasPrefix(input, "/") { - handleCommand(input, k) - continue - } - - // Send message to LLM agent via kernel - _, err := k.SendMessage("user", "llm", input) - if err != nil { - fmt.Printf("Error: %v\n", err) - continue - } - fmt.Println() - } - - if err := scanner.Err(); err != nil { - fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err) - } - close(done) - }() - - // Wait for either SIGINT or REPL exit - select { - case <-sig: - fmt.Println("\nShutting down Orca kernel...") - case <-done: - fmt.Println("\nInput closed. Shutting down Orca kernel...") + if _, err := p.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err) + os.Exit(1) } if err := k.Stop(); err != nil { - log.Fatalf("Failed to stop kernel: %v", err) - } - fmt.Println("Orca kernel shut down gracefully.") -} - -// handleCommand processes REPL commands. -func handleCommand(cmd string, k *kernel.Kernel) { - switch cmd { - case "/help": - fmt.Println("Available commands:") - fmt.Println(" /help - Show this help message") - fmt.Println(" /exit - Exit the program") - fmt.Println(" /quit - Exit the program") - fmt.Println(" /plugins - List registered plugins") - fmt.Println(" /agents - List active agents") - fmt.Println(" /tools - List registered tools") - fmt.Println(" /skills - List loaded skills") - fmt.Println(" /status - Show kernel status") - fmt.Println() - fmt.Println("Any other input is sent to the LLM agent for processing.") - - case "/exit", "/quit": - fmt.Println("Goodbye!") - os.Exit(0) - - case "/plugins": - plugins := k.ListPlugins() - if len(plugins) == 0 { - fmt.Println("No plugins registered.") - } else { - fmt.Println("Registered plugins:") - for _, p := range plugins { - fmt.Printf(" - %s (%s)\n", p.Name(), p.Version()) - } - } - - case "/agents": - as := k.ActorSystem() - if as == nil { - fmt.Println("Actor system not initialized.") - return - } - infos := as.AgentInfos() - if len(infos) == 0 { - fmt.Println("No agents running.") - } else { - fmt.Println("Active agents:") - for _, info := range infos { - fmt.Printf(" - %s [%s] (status: %s)\n", info.ID, info.Role, info.Status) - } - } - - case "/tools": - tm := k.ToolManager() - if tm == nil { - fmt.Println("Tool manager not initialized.") - return - } - tools := tm.List() - if len(tools) == 0 { - fmt.Println("No tools registered.") - } else { - fmt.Println("Registered tools:") - for _, t := range tools { - fmt.Printf(" - %s: %s\n", t.Name(), t.Description()) - } - } - - case "/skills": - sm := k.SkillManager() - if sm == nil { - fmt.Println("Skill manager not initialized.") - return - } - skills := sm.ListSkills() - if len(skills) == 0 { - fmt.Println("No skills loaded.") - } else { - fmt.Println("Loaded skills:") - for _, s := range skills { - fmt.Printf(" - %s: %s\n", s.Name, s.Description) - } - } - - case "/status": - fmt.Printf("Kernel running: %v\n", k.IsRunning()) - if tm := k.ToolManager(); tm != nil { - fmt.Printf("Tools registered: %d\n", tm.Count()) - } - if as := k.ActorSystem(); as != nil { - fmt.Printf("Agents active: %d\n", as.AgentCount()) - } - if sm := k.SkillManager(); sm != nil { - fmt.Printf("Skills loaded: %d\n", len(sm.ListSkills())) - } - - default: - fmt.Printf("Unknown command: %s\n", cmd) - fmt.Println("Type /help for available commands.") + log.Printf("Warning: error stopping kernel: %v", err) } } diff --git a/go.mod b/go.mod index 7f16df4..f98b96d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,43 @@ module github.com/orca/orca go 1.26.1 + +require ( + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/alecthomas/chroma/v2 v2.20.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/charmbracelet/bubbles v1.0.0 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/glamour v1.0.0 // indirect + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.9.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.13 // indirect + github.com/yuin/goldmark-emoji v1.0.6 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fd0c03f --- /dev/null +++ b/go.sum @@ -0,0 +1,91 @@ +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= +github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= +github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08= +github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= +github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= +github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= +github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= +github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= diff --git a/internal/config/config.go b/internal/config/config.go index 5566f1b..d497a4d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,131 +1,174 @@ +// Package config 为 Orca 框架提供配置管理功能。 +// +// 配置从 TOML 文件加载,默认路径为 ~/.orca/config.toml。 +// 所有模型参数和 Agent 身份描述均通过配置文件管理, +// 不再从环境变量读取。 package config import ( + "fmt" "os" - "strconv" + "path/filepath" "time" + + "github.com/BurntSushi/toml" ) const ( - ProviderOllama = "ollama" + // ProviderOllama 选择本地 Ollama LLM 后端。 + ProviderOllama = "ollama" + // ProviderDeepSeek 选择云端 DeepSeek LLM 后端。 ProviderDeepSeek = "deepseek" ) +// Config 保存 Orca 框架的所有配置信息。 type Config struct { - Provider string `json:"provider"` - Ollama OllamaConfig `json:"ollama"` - DeepSeek DeepSeekConfig `json:"deepseek"` - Sandbox SandboxConfig `json:"sandbox"` - Session SessionConfig `json:"session"` + Provider string `toml:"provider"` + Ollama OllamaConfig `toml:"ollama"` + DeepSeek DeepSeekConfig `toml:"deepseek"` + Sandbox SandboxConfig `toml:"sandbox"` + Session SessionConfig `toml:"session"` + Agent AgentConfig `toml:"agent"` } +// OllamaConfig 保存 Ollama LLM 后端的设置。 type OllamaConfig struct { - BaseURL string `json:"base_url"` - Model string `json:"model"` - Timeout time.Duration `json:"timeout"` + BaseURL string `toml:"base_url"` + Model string `toml:"model"` + Timeout time.Duration `toml:"timeout"` } +// DeepSeekConfig 保存 DeepSeek LLM 后端的设置。 type DeepSeekConfig struct { - BaseURL string `json:"base_url"` - Model string `json:"model"` - APIKey string `json:"api_key"` - Timeout time.Duration `json:"timeout"` + BaseURL string `toml:"base_url"` + Model string `toml:"model"` + APIKey string `toml:"api_key"` + Timeout time.Duration `toml:"timeout"` } +// SandboxConfig 保存命令执行沙箱的设置。 type SandboxConfig struct { - Timeout time.Duration `json:"timeout"` - MaxMemory int64 `json:"max_memory"` - WorkingDir string `json:"working_dir"` + Timeout time.Duration `toml:"timeout"` + MaxMemory int64 `toml:"max_memory"` + WorkingDir string `toml:"working_dir"` } +// SessionConfig 保存对话会话存储的设置。 type SessionConfig struct { - StorageDir string `json:"storage_dir"` - MaxHistory int `json:"max_history"` + StorageDir string `toml:"storage_dir"` + MaxHistory int `toml:"max_history"` } +// AgentConfig 保存 Agent 身份和行为的配置。 +type AgentConfig struct { + // Role 是 Agent 的角色标识,如 "assistant", "coder", "reviewer" 等。 + Role string `toml:"role"` + + // SystemPrompt 是 Agent 的系统提示词(直接内联配置)。 + // 如果 PromptFile 也配置了,PromptFile 优先级更高。 + SystemPrompt string `toml:"system_prompt"` + + // PromptFile 是外部提示词文件的路径(相对于 ~/.orca/ 或绝对路径)。 + // 如果文件存在,其内容会作为 system prompt 使用。 + PromptFile string `toml:"prompt_file"` +} + +// DefaultConfig 返回默认配置。 +// 注意:模型相关默认值均为空字符串,要求用户必须在 config.toml 中配置。 func DefaultConfig() *Config { + home, _ := os.UserHomeDir() + return &Config{ Provider: ProviderDeepSeek, Ollama: OllamaConfig{ BaseURL: "http://localhost:11434", - Model: "gemma4:e4b", + Model: "", Timeout: 120 * time.Second, }, DeepSeek: DeepSeekConfig{ BaseURL: "https://api.deepseek.com/v1", - Model: "deepseek-v4-flash", - APIKey: "sk-2f1049148e06492dbc304ba49c81c321", + Model: "", + APIKey: "", Timeout: 120 * time.Second, }, Sandbox: SandboxConfig{ Timeout: 30 * time.Second, MaxMemory: 512 * 1024 * 1024, - WorkingDir: "/tmp/orca/sandbox", + WorkingDir: filepath.Join(home, ".orca", "sandbox"), }, Session: SessionConfig{ - StorageDir: func() string { - home, _ := os.UserHomeDir() - return home + "/.orca/sessions" - }(), + StorageDir: filepath.Join(home, ".orca", "sessions"), MaxHistory: 100, }, + Agent: AgentConfig{ + Role: "assistant", + SystemPrompt: "", + PromptFile: "", + }, } } -func LoadConfigFromEnv() *Config { +// LoadConfigFromFile 从指定路径加载 TOML 配置文件。 +// 如果文件不存在,返回默认配置。 +func LoadConfigFromFile(path string) (*Config, error) { cfg := DefaultConfig() - if v := os.Getenv("ORCA_PROVIDER"); v != "" { - cfg.Provider = v + if _, err := os.Stat(path); os.IsNotExist(err) { + return cfg, nil } - if v := os.Getenv("ORCA_OLLAMA_BASE_URL"); v != "" { - cfg.Ollama.BaseURL = v + + if _, err := toml.DecodeFile(path, cfg); err != nil { + return nil, fmt.Errorf("config: failed to load %q: %w", path, err) } - if v := os.Getenv("ORCA_OLLAMA_MODEL"); v != "" { - cfg.Ollama.Model = v + + return cfg, nil +} + +// LoadConfig 从默认路径加载配置。 +// 默认路径优先级: +// 1. ./config.toml(当前工作目录) +// 2. ~/.orca/config.toml +func LoadConfig() (*Config, error) { + // 尝试当前目录 + if _, err := os.Stat("config.toml"); err == nil { + return LoadConfigFromFile("config.toml") } - if v := os.Getenv("ORCA_OLLAMA_TIMEOUT"); v != "" { - if d, err := time.ParseDuration(v); err == nil { - cfg.Ollama.Timeout = d + + // 尝试用户主目录 + home, err := os.UserHomeDir() + if err != nil { + return DefaultConfig(), nil + } + + defaultPath := filepath.Join(home, ".orca", "config.toml") + return LoadConfigFromFile(defaultPath) +} + +// GetSystemPrompt 返回 Agent 的系统提示词。 +// 优先级:PromptFile > SystemPrompt > 空字符串。 +func (c *Config) GetSystemPrompt() string { + // 如果配置了外部文件,优先读取文件 + if c.Agent.PromptFile != "" { + path := c.Agent.PromptFile + // 如果是相对路径,基于 ~/.orca/ 解析 + if !filepath.IsAbs(path) { + home, _ := os.UserHomeDir() + path = filepath.Join(home, ".orca", path) } - } - if v := os.Getenv("ORCA_DEEPSEEK_BASE_URL"); v != "" { - cfg.DeepSeek.BaseURL = v - } - if v := os.Getenv("ORCA_DEEPSEEK_MODEL"); v != "" { - cfg.DeepSeek.Model = v - } - if v := os.Getenv("ORCA_DEEPSEEK_API_KEY"); v != "" { - cfg.DeepSeek.APIKey = v - } - if v := os.Getenv("ORCA_DEEPSEEK_TIMEOUT"); v != "" { - if d, err := time.ParseDuration(v); err == nil { - cfg.DeepSeek.Timeout = d - } - } - if v := os.Getenv("ORCA_SANDBOX_TIMEOUT"); v != "" { - if d, err := time.ParseDuration(v); err == nil { - cfg.Sandbox.Timeout = d - } - } - if v := os.Getenv("ORCA_SANDBOX_MAX_MEMORY"); v != "" { - if n, err := strconv.ParseInt(v, 10, 64); err == nil { - cfg.Sandbox.MaxMemory = n - } - } - if v := os.Getenv("ORCA_SANDBOX_WORKING_DIR"); v != "" { - cfg.Sandbox.WorkingDir = v - } - if v := os.Getenv("ORCA_SESSION_STORAGE_DIR"); v != "" { - cfg.Session.StorageDir = v - } - if v := os.Getenv("ORCA_SESSION_MAX_HISTORY"); v != "" { - if n, err := strconv.Atoi(v); err == nil { - cfg.Session.MaxHistory = n + + if data, err := os.ReadFile(path); err == nil { + return string(data) } } - return cfg + // 返回内联配置的 prompt + return c.Agent.SystemPrompt +} + +// GetPromptsDir 返回提示词文件目录。 +func GetPromptsDir() string { + home, _ := os.UserHomeDir() + return filepath.Join(home, ".orca", "prompts") } func (c *Config) IsValid() error { diff --git a/internal/tui/model.go b/internal/tui/model.go new file mode 100644 index 0000000..878d9df --- /dev/null +++ b/internal/tui/model.go @@ -0,0 +1,330 @@ +package tui + +import ( + "fmt" + "strings" + "time" + + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/orca/orca/pkg/actor" + "github.com/orca/orca/pkg/kernel" +) + +type ChatMessage struct { + Role string + Content string + Agent string +} + +type Model struct { + width int + height int + kernel *kernel.Kernel + viewport viewport.Model + textarea textarea.Model + messages []ChatMessage + events chan Event + writer *EventWriter + ready bool + loading bool + agentRuns map[string]bool +} + +func (m Model) TestEvents() chan Event { + return m.events +} + +func (m Model) MessageCount() int { + return len(m.messages) +} + +func (m Model) LastMessage() ChatMessage { + if len(m.messages) == 0 { + return ChatMessage{} + } + return m.messages[len(m.messages)-1] +} + +func (m *Model) HandleEventForTest(ev Event) { + m.handleEvent(ev) +} + +func NewModel(k *kernel.Kernel) Model { + ta := textarea.New() + ta.Placeholder = "Type a message and press Enter..." + ta.SetWidth(80) + ta.SetHeight(3) + ta.Focus() + ta.ShowLineNumbers = false + ta.KeyMap.InsertNewline.SetEnabled(false) + + evCh := make(chan Event, 256) + w := NewEventWriter(evCh) + k.SetStreamWriter(w) + + return Model{ + kernel: k, + textarea: ta, + events: evCh, + writer: w, + agentRuns: make(map[string]bool), + } +} + +func (m Model) Init() tea.Cmd { + return tea.Batch( + textarea.Blink, + m.waitEvent(), + m.tickRefresh(), + m.flushWriter(), + ) +} + +func (m Model) waitEvent() tea.Cmd { + return func() tea.Msg { + return <-m.events + } +} + +func (m Model) tickRefresh() tea.Cmd { + return tea.Tick(time.Second, func(t time.Time) tea.Msg { + return refreshMsg{} + }) +} + +type refreshMsg struct{} +type flushMsg struct{} + +func (m Model) flushWriter() tea.Cmd { + return tea.Tick(150*time.Millisecond, func(t time.Time) tea.Msg { + m.writer.Flush() + return flushMsg{} + }) +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + m.updateLayout() + + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + case tea.KeyEnter: + if m.loading { + return m, nil + } + input := strings.TrimSpace(m.textarea.Value()) + if input == "" { + return m, nil + } + m.textarea.SetValue("") + m.messages = append(m.messages, ChatMessage{Role: "user", Content: input}) + m.updateViewportContent() + m.loading = true + return m, tea.Batch(m.sendMessage(input), m.waitEvent()) + } + + case Event: + m.handleEvent(msg) + return m, m.waitEvent() + + case refreshMsg: + m.refreshAgentStatus() + return m, m.tickRefresh() + + case flushMsg: + return m, m.flushWriter() + + case responseMsg: + m.loading = false + m.writer.Flush() + if msg.err != nil { + m.messages = append(m.messages, ChatMessage{Role: "system", Content: fmt.Sprintf("Error: %v", msg.err)}) + m.updateViewportContent() + } + return m, nil + } + + var cmd tea.Cmd + m.textarea, cmd = m.textarea.Update(msg) + cmds = append(cmds, cmd) + + m.viewport, cmd = m.viewport.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +type responseMsg struct { + content string + err error +} + +func (m Model) sendMessage(input string) tea.Cmd { + return func() tea.Msg { + resp, err := m.kernel.SendMessage("user", "llm", input) + return responseMsg{content: resp, err: err} + } +} + +func (m *Model) handleEvent(ev Event) { + switch ev.Type { + case "stream": + if len(m.messages) > 0 && m.messages[len(m.messages)-1].Role == "assistant" { + last := &m.messages[len(m.messages)-1] + last.Content += ev.Content + } else { + m.messages = append(m.messages, ChatMessage{Role: "assistant", Content: ev.Content}) + } + m.updateViewportContent() + case "agent_start": + m.agentRuns[ev.Agent] = true + case "agent_end": + m.agentRuns[ev.Agent] = false + } +} + +func (m *Model) refreshAgentStatus() { + if as := m.kernel.ActorSystem(); as != nil { + for _, info := range as.AgentInfos() { + if info.Status == actor.StatusProcessing { + m.agentRuns[info.ID] = true + } else { + m.agentRuns[info.ID] = false + } + } + } +} + +func (m *Model) updateLayout() { + if m.width == 0 || m.height == 0 { + return + } + + rightWidth := 42 + leftWidth := m.width - rightWidth - 3 + chatHeight := m.height - 5 + + m.textarea.SetWidth(leftWidth) + m.viewport.Width = leftWidth + m.viewport.Height = chatHeight + + if !m.ready { + m.viewport = viewport.New(leftWidth, chatHeight) + m.viewport.SetContent("") + m.ready = true + } else { + m.viewport.Width = leftWidth + m.viewport.Height = chatHeight + } +} + +func (m *Model) updateViewportContent() { + var b strings.Builder + for _, msg := range m.messages { + b.WriteString(m.formatMessage(msg)) + b.WriteString("\n\n") + } + m.viewport.SetContent(b.String()) + m.viewport.GotoBottom() +} + +func (m Model) formatMessage(msg ChatMessage) string { + switch msg.Role { + case "user": + return userStyle.Render("You: ") + "\n" + msg.Content + case "assistant": + if msg.Agent != "" { + return agentStyle.Render(fmt.Sprintf("[%s] ", msg.Agent)) + "\n" + msg.Content + } + return agentStyle.Render("Assistant: ") + "\n" + msg.Content + case "system": + return systemStyle.Render("System: ") + "\n" + msg.Content + default: + return msg.Content + } +} + +func (m Model) View() string { + if m.width == 0 || m.height == 0 { + return "Initializing..." + } + + rightWidth := 42 + leftWidth := m.width - rightWidth - 3 + chatHeight := m.height - 5 + + m.textarea.SetWidth(leftWidth) + m.viewport.Width = leftWidth + m.viewport.Height = chatHeight + + chatBox := boxStyle.Width(leftWidth).Height(chatHeight).Render(m.viewport.View()) + inputBox := boxStyle.Width(leftWidth).Render(m.textarea.View()) + leftPanel := lipgloss.JoinVertical(lipgloss.Left, chatBox, inputBox) + + stats := m.renderStats() + agents := m.renderAgents() + empty := boxStyle.Width(rightWidth - 4).Height(m.height - 20).Render("") + rightPanel := lipgloss.JoinVertical(lipgloss.Left, stats, agents, empty) + + if m.loading { + leftPanel = lipgloss.JoinVertical(lipgloss.Left, leftPanel, processingStyle.Render("Processing...")) + } + + return lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, rightPanel) +} + +func (m Model) renderStats() string { + var b strings.Builder + b.WriteString(titleStyle.Render("Statistics") + "\n\n") + + tools := 0 + if tm := m.kernel.ToolManager(); tm != nil { + tools = tm.Count() + } + skills := 0 + if sm := m.kernel.SkillManager(); sm != nil { + skills = len(sm.ListSkills()) + } + agents := 0 + if as := m.kernel.ActorSystem(); as != nil { + agents = as.AgentCount() + } + + b.WriteString(statLabelStyle.Render("Tools: ")) + b.WriteString(statValueStyle.Render(fmt.Sprintf("%d", tools)) + "\n") + b.WriteString(statLabelStyle.Render("Skills: ")) + b.WriteString(statValueStyle.Render(fmt.Sprintf("%d", skills)) + "\n") + b.WriteString(statLabelStyle.Render("Agents: ")) + b.WriteString(statValueStyle.Render(fmt.Sprintf("%d", agents)) + "\n") + + return boxStyle.Width(38).Render(b.String()) +} + +func (m Model) renderAgents() string { + var b strings.Builder + b.WriteString(titleStyle.Render("Active Agents") + "\n\n") + + if as := m.kernel.ActorSystem(); as != nil { + for _, info := range as.AgentInfos() { + status := "idle" + style := idleAgentStyle + if info.Status == actor.StatusProcessing || m.agentRuns[info.ID] { + status = "running" + style = activeAgentStyle + } + b.WriteString(fmt.Sprintf("• %s: %s\n", info.ID, style.Render(status))) + } + } + + return boxStyle.Width(38).Render(b.String()) +} diff --git a/internal/tui/styles.go b/internal/tui/styles.go new file mode 100644 index 0000000..b520611 --- /dev/null +++ b/internal/tui/styles.go @@ -0,0 +1,73 @@ +package tui + +import "github.com/charmbracelet/lipgloss" + +var ( + colors = struct { + primary string + secondary string + accent string + success string + warning string + danger string + muted string + bg string + border string + }{ + primary: "#7AA2F7", + secondary: "#BB9AF7", + accent: "#7DCFFF", + success: "#9ECE6A", + warning: "#E0AF68", + danger: "#F7768E", + muted: "#565F89", + bg: "#1A1B26", + border: "#414868", + } + + baseStyle = lipgloss.NewStyle(). + Background(lipgloss.Color(colors.bg)) + + titleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color(colors.primary)). + Padding(0, 1) + + boxStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color(colors.border)). + Padding(1, 2). + Background(lipgloss.Color(colors.bg)) + + userStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(colors.accent)). + Bold(true) + + agentStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(colors.success)). + Bold(true) + + systemStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(colors.warning)). + Bold(true) + + mutedStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(colors.muted)) + + statValueStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color(colors.primary)) + + statLabelStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(colors.muted)) + + activeAgentStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(colors.success)) + + idleAgentStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(colors.muted)) + + processingStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(colors.warning)). + Bold(true) +) diff --git a/internal/tui/writer.go b/internal/tui/writer.go new file mode 100644 index 0000000..40a8d96 --- /dev/null +++ b/internal/tui/writer.go @@ -0,0 +1,51 @@ +package tui + +import ( + "strings" + "sync" + "time" +) + +type Event struct { + Type string + Content string + Agent string +} + +type EventWriter struct { + ch chan Event + mu sync.Mutex + buffer strings.Builder +} + +func NewEventWriter(ch chan Event) *EventWriter { + return &EventWriter{ch: ch} +} + +func (w *EventWriter) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + w.buffer.Write(p) + content := w.buffer.String() + if strings.Contains(content, "\n") || len(content) > 80 { + w.sendEvent(Event{Type: "stream", Content: content}) + w.buffer.Reset() + } + return len(p), nil +} + +func (w *EventWriter) Flush() { + w.mu.Lock() + defer w.mu.Unlock() + if w.buffer.Len() > 0 { + w.sendEvent(Event{Type: "stream", Content: w.buffer.String()}) + w.buffer.Reset() + } +} + +func (w *EventWriter) sendEvent(ev Event) { + select { + case w.ch <- ev: + case <-time.After(100 * time.Millisecond): + } +} diff --git a/pkg/actor/llm_agent.go b/pkg/actor/llm_agent.go index e1790bc..7869e78 100644 --- a/pkg/actor/llm_agent.go +++ b/pkg/actor/llm_agent.go @@ -6,10 +6,12 @@ import ( "fmt" "io" "strings" + "sync" "github.com/orca/orca/pkg/bus" "github.com/orca/orca/pkg/llm" "github.com/orca/orca/pkg/session" + "github.com/orca/orca/pkg/skill" "github.com/orca/orca/pkg/tool" ) @@ -25,9 +27,12 @@ type LLMAgent struct { sessionMgr *session.Manager sessionID string toolManager *tool.Manager + skillManager *skill.Manager toolWorker *ToolWorker windowSize int streamWriter io.Writer + systemPrompt string + subAgents map[string]string } // LLMAgentOption is a functional option for configuring the LLMAgent. @@ -68,6 +73,12 @@ func WithWindowSize(size int) LLMAgentOption { } } +func WithSkillManager(mgr *skill.Manager) LLMAgentOption { + return func(a *LLMAgent) { + a.skillManager = mgr + } +} + // WithStreamWriter sets the writer for streaming LLM output. func WithStreamWriter(w io.Writer) LLMAgentOption { return func(a *LLMAgent) { @@ -75,6 +86,18 @@ func WithStreamWriter(w io.Writer) LLMAgentOption { } } +func WithSystemPrompt(prompt string) LLMAgentOption { + return func(a *LLMAgent) { + a.systemPrompt = prompt + } +} + +func WithSubAgents(agents map[string]string) LLMAgentOption { + return func(a *LLMAgent) { + a.subAgents = agents + } +} + // NewLLMAgent creates a new LLMAgent with the given LLM backend and options. // The agent is started automatically upon creation. func NewLLMAgent(id string, backend llm.LLM, opts ...LLMAgentOption) *LLMAgent { @@ -139,9 +162,20 @@ func (a *LLMAgent) handleUserMessage(ctx context.Context, msg bus.Message) (bus. a.sessionMgr.AddMessage(a.sessionID, session.RoleUser, content, nil) } - // Build LLM messages from session context llmMessages := a.buildLLMMessages() + if a.skillManager != nil { + matchedSkills := a.skillManager.FindSkill(content) + for _, s := range matchedSkills { + if s.Body != "" { + llmMessages = append(llmMessages, llm.Message{ + Role: "system", + Content: fmt.Sprintf("以下是你需要遵循的 %s 技能指南:\n\n%s", s.Name, s.Body), + }) + } + } + } + // Call LLM (potentially multiple rounds for tool calls) finalResponse, err := a.chatWithToolLoop(ctx, llmMessages) if err != nil { @@ -165,10 +199,20 @@ func (a *LLMAgent) handleUserMessage(ctx context.Context, msg bus.Message) (bus. func (a *LLMAgent) buildLLMMessages() []llm.Message { messages := make([]llm.Message, 0) - if a.toolManager != nil { + // 1. 用户自定义 system prompt(配置式身份描述) + if a.systemPrompt != "" { messages = append(messages, llm.Message{ Role: "system", - Content: a.buildToolSystemPrompt(), + Content: a.systemPrompt, + }) + } + + // 2. 运行时工具说明(动态生成) + toolPrompt := a.buildToolPrompt() + if toolPrompt != "" { + messages = append(messages, llm.Message{ + Role: "system", + Content: toolPrompt, }) } @@ -195,31 +239,69 @@ func (a *LLMAgent) buildLLMMessages() []llm.Message { return messages } -// buildToolSystemPrompt creates a system prompt describing all available tools. -// This enables prompt-based tool calling for models without native function -// calling support. -func (a *LLMAgent) buildToolSystemPrompt() string { - if a.toolManager == nil { +// buildToolPrompt 生成工具说明提示词(不包含身份描述)。 +// 将可用工具和调用规则注入给 LLM,支持基于提示词的工具调用。 +func (a *LLMAgent) buildToolPrompt() string { + var b strings.Builder + + if a.toolManager != nil { + b.WriteString("你可以使用以下工具来完成用户的请求。\n\n") + b.WriteString("可用工具列表:\n") + + for _, t := range a.toolManager.List() { + b.WriteString(fmt.Sprintf("\n工具名: %s\n", t.Name())) + b.WriteString(fmt.Sprintf("描述: %s\n", t.Description())) + paramsJSON, _ := json.Marshal(t.Parameters()) + b.WriteString(fmt.Sprintf("参数: %s\n", string(paramsJSON))) + } + + b.WriteString("\n规则:\n") + b.WriteString("1. 当你需要调用工具时,请在回复中**只输出**以下 JSON 格式(不要添加其他文字):\n") + b.WriteString(` {"tool": "工具名", "arguments": {"参数名": "参数值"}}` + "\n") + b.WriteString("2. 如果需要同时调用多个工具(并行执行),请输出 JSON 数组格式:\n") + b.WriteString(` [{"tool": "工具名1", "arguments": {...}}, {"tool": "工具名2", "arguments": {...}}]` + "\n") + b.WriteString("3. 如果你已经看到了工具返回的结果,请直接根据结果回答用户,不要再次调用工具。\n") + b.WriteString("4. 如果你不需要调用工具,请直接回复用户。\n") + } + + if len(a.subAgents) > 0 { + b.WriteString("\n\n你可以调用以下专业Agent来协助完成特定任务:\n") + for name, description := range a.subAgents { + b.WriteString(fmt.Sprintf("- %s: %s\n", name, description)) + } + b.WriteString("\n调用方式:使用 agent_call 工具,指定 agent 名称和任务描述。\n") + b.WriteString("示例:{\"tool\": \"agent_call\", \"arguments\": {\"agent\": \"coder\", \"task\": \"写个快速排序\"}}\n") + b.WriteString("如果用户有多个独立任务,请同时调用多个 agent_call(JSON数组格式),让它们并行执行。\n") + b.WriteString("\n重要:当用户的请求涉及上述专业领域时,你必须调用相应的子Agent,不要自己直接回答。\n") + } + + if a.skillManager != nil { + skills := a.skillManager.ListSkills() + if len(skills) > 0 { + b.WriteString("\n\n你还可以使用以下技能来更好地帮助用户:\n") + for _, s := range skills { + b.WriteString(fmt.Sprintf("\n=== 技能: %s ===\n", s.Name)) + b.WriteString(fmt.Sprintf("描述: %s\n", s.Description)) + if len(s.Triggers) > 0 { + b.WriteString(fmt.Sprintf("触发词: %s\n", strings.Join(s.Triggers, ", "))) + } + if s.Body != "" { + body := s.Body + if len(body) > 4000 { + body = body[:4000] + "\n...[内容已截断]" + } + b.WriteString(fmt.Sprintf("\n详细指南:\n%s\n", body)) + } + b.WriteString(fmt.Sprintf("=== 结束: %s ===\n", s.Name)) + } + b.WriteString("\n当用户的请求匹配某个技能的触发词时,请根据该技能的详细指南提供更专业的帮助。\n") + b.WriteString("你应该主动使用相关技能的专业知识来回答用户问题,而不需要询问用户是否使用技能。\n") + } + } + + if b.Len() == 0 { return "" } - - var b strings.Builder - b.WriteString("你是一个 AI 助手,可以使用以下工具来完成用户的请求。\n\n") - b.WriteString("可用工具列表:\n") - - for _, t := range a.toolManager.List() { - b.WriteString(fmt.Sprintf("\n工具名: %s\n", t.Name())) - b.WriteString(fmt.Sprintf("描述: %s\n", t.Description())) - paramsJSON, _ := json.Marshal(t.Parameters()) - b.WriteString(fmt.Sprintf("参数: %s\n", string(paramsJSON))) - } - - b.WriteString("\n规则:\n") - b.WriteString("1. 当你需要调用工具时,请在回复中**只输出**以下 JSON 格式(不要添加其他文字):\n") - b.WriteString(` {"tool": "工具名", "arguments": {"参数名": "参数值"}}` + "\n") - b.WriteString("2. 如果你已经看到了工具返回的结果,请直接根据结果回答用户,不要再次调用工具。\n") - b.WriteString("3. 如果你不需要调用工具,请直接回复用户。\n") - return b.String() } @@ -249,11 +331,11 @@ func (a *LLMAgent) chatWithToolLoop(ctx context.Context, messages []llm.Message) Content: content, }) - for _, tc := range toolCalls { - resultContent := a.executeToolCall(ctx, tc) + results := a.executeToolCallsParallel(ctx, toolCalls) + for _, result := range results { messages = append(messages, llm.Message{ Role: "user", - Content: fmt.Sprintf("工具 %s 的执行结果:%s", tc.Function.Name, resultContent), + Content: result, }) } } @@ -298,30 +380,104 @@ func (a *LLMAgent) streamChat(ctx context.Context, messages []llm.Message) (stri } func (a *LLMAgent) parseToolCallsFromContent(content string) []llm.ToolCall { + cleanContent := a.extractJSONFromMarkdown(content) + + var toolCalls []llm.ToolCall + var callIndex int + + var parsedList []struct { + Tool string `json:"tool"` + Arguments map[string]interface{} `json:"arguments"` + } + if err := json.Unmarshal([]byte(cleanContent), &parsedList); err == nil && len(parsedList) > 0 { + for _, parsed := range parsedList { + if parsed.Tool == "" { + continue + } + argsJSON, _ := json.Marshal(parsed.Arguments) + toolCalls = append(toolCalls, llm.ToolCall{ + ID: fmt.Sprintf("call_%d", callIndex), + Type: "function", + Function: llm.FunctionCall{ + Name: parsed.Tool, + Arguments: string(argsJSON), + }, + }) + callIndex++ + } + return toolCalls + } + var parsed struct { Tool string `json:"tool"` Arguments map[string]interface{} `json:"arguments"` } - if err := json.Unmarshal([]byte(content), &parsed); err != nil || parsed.Tool == "" { - return nil + if err := json.Unmarshal([]byte(cleanContent), &parsed); err == nil && parsed.Tool != "" { + argsJSON, _ := json.Marshal(parsed.Arguments) + return []llm.ToolCall{{ + ID: "call_0", + Type: "function", + Function: llm.FunctionCall{ + Name: parsed.Tool, + Arguments: string(argsJSON), + }, + }} } - argsJSON, _ := json.Marshal(parsed.Arguments) - return []llm.ToolCall{{ - ID: "call_0", - Type: "function", - Function: llm.FunctionCall{ - Name: parsed.Tool, - Arguments: string(argsJSON), - }, - }} + var commaSeparated []struct { + Tool string `json:"tool"` + Arguments map[string]interface{} `json:"arguments"` + } + wrapped := "[" + cleanContent + "]" + if err := json.Unmarshal([]byte(wrapped), &commaSeparated); err == nil && len(commaSeparated) > 0 { + for _, parsed := range commaSeparated { + if parsed.Tool == "" { + continue + } + argsJSON, _ := json.Marshal(parsed.Arguments) + toolCalls = append(toolCalls, llm.ToolCall{ + ID: fmt.Sprintf("call_%d", callIndex), + Type: "function", + Function: llm.FunctionCall{ + Name: parsed.Tool, + Arguments: string(argsJSON), + }, + }) + callIndex++ + } + return toolCalls + } + + return nil +} + +func (a *LLMAgent) extractJSONFromMarkdown(content string) string { + start := strings.Index(content, "```") + if start == -1 { + return content + } + + start = strings.Index(content[start:], "\n") + if start == -1 { + return content + } + start++ + + end := strings.LastIndex(content[start:], "```") + if end == -1 { + return content + } + + return strings.TrimSpace(content[start : start+end]) } -// executeToolCall runs a single tool call and returns the result as a JSON string. func (a *LLMAgent) executeToolCall(ctx context.Context, tc llm.ToolCall) string { toolName := tc.Function.Name - // Parse arguments + if a.streamWriter != nil { + fmt.Fprintf(a.streamWriter, "\n[正在执行工具: %s...]\n", toolName) + } + var args map[string]interface{} if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil { args = map[string]interface{}{ @@ -329,9 +485,7 @@ func (a *LLMAgent) executeToolCall(ctx context.Context, tc llm.ToolCall) string } } - // Execute via ToolWorker (preferred) or directly via tool.Manager if a.toolWorker != nil { - // Create a tool call message for the ToolWorker toolCallMsg := bus.Message{ ID: tc.ID, Type: bus.MsgTypeToolCall, @@ -341,11 +495,13 @@ func (a *LLMAgent) executeToolCall(ctx context.Context, tc llm.ToolCall) string } resultMsg, err := a.toolWorker.Process(ctx, toolCallMsg) + if a.streamWriter != nil { + fmt.Fprintf(a.streamWriter, "[工具 %s 执行完成]\n", toolName) + } if err != nil { return fmt.Sprintf(`{"error": "tool execution failed: %v"}`, err) } - // Serialize the result resultJSON, err := json.Marshal(resultMsg.Content) if err != nil { return fmt.Sprintf(`{"error": "failed to marshal result: %v"}`, err) @@ -353,9 +509,11 @@ func (a *LLMAgent) executeToolCall(ctx context.Context, tc llm.ToolCall) string return string(resultJSON) } - // Fallback: execute directly via tool.Manager if a.toolManager != nil { result, err := a.toolManager.Execute(toolName, ctx, args) + if a.streamWriter != nil { + fmt.Fprintf(a.streamWriter, "[工具 %s 执行完成]\n", toolName) + } if err != nil { return fmt.Sprintf(`{"error": "tool execution failed: %v"}`, err) } @@ -370,6 +528,41 @@ func (a *LLMAgent) executeToolCall(ctx context.Context, tc llm.ToolCall) string return fmt.Sprintf(`{"error": "no tool worker or tool manager available for %q"}`, toolName) } +func (a *LLMAgent) executeToolCallsParallel(ctx context.Context, toolCalls []llm.ToolCall) []string { + if len(toolCalls) == 1 { + result := a.executeToolCall(ctx, toolCalls[0]) + return []string{fmt.Sprintf("工具 %s 的执行结果:%s", toolCalls[0].Function.Name, result)} + } + + type result struct { + index int + content string + } + + results := make([]result, len(toolCalls)) + var wg sync.WaitGroup + + for i, tc := range toolCalls { + wg.Add(1) + go func(idx int, toolCall llm.ToolCall) { + defer wg.Done() + res := a.executeToolCall(ctx, toolCall) + results[idx] = result{ + index: idx, + content: fmt.Sprintf("工具 %s 的执行结果:%s", toolCall.Function.Name, res), + } + }(i, tc) + } + + wg.Wait() + + strings := make([]string, len(toolCalls)) + for i, r := range results { + strings[i] = r.content + } + return strings +} + // handleSystem processes internal system messages. func (a *LLMAgent) handleSystem(ctx context.Context, msg bus.Message) (bus.Message, error) { return bus.Message{ diff --git a/pkg/actor/orchestrator.go b/pkg/actor/orchestrator.go index 5f98c15..fa2a7b7 100644 --- a/pkg/actor/orchestrator.go +++ b/pkg/actor/orchestrator.go @@ -15,9 +15,10 @@ import ( // agents and can dynamically add or remove them. type Orchestrator struct { *BaseAgent - workers map[string]Agent - bus bus.MessageBus - mu sync.RWMutex + workers map[string]Agent + defaultWorker Agent + bus bus.MessageBus + mu sync.RWMutex } // NewOrchestrator creates a new Orchestrator agent with the given id and message bus. @@ -49,7 +50,6 @@ func (o *Orchestrator) handleMessage(ctx context.Context, msg bus.Message) (bus. } } -// handleTask processes a task request by delegating to an available worker. func (o *Orchestrator) handleTask(ctx context.Context, msg bus.Message) (bus.Message, error) { o.mu.RLock() defer o.mu.RUnlock() @@ -58,7 +58,21 @@ func (o *Orchestrator) handleTask(ctx context.Context, msg bus.Message) (bus.Mes return bus.Message{}, fmt.Errorf("orchestrator %s: no workers available", o.ID()) } - // Simple round-robin: pick the first available worker + if msg.To != "" { + if w, ok := o.workers[msg.To]; ok { + return w.Process(ctx, msg) + } + for _, w := range o.workers { + if w.Role() == msg.To || containsIgnoreCase(w.Role(), msg.To) { + return w.Process(ctx, msg) + } + } + } + + if o.defaultWorker != nil { + return o.defaultWorker.Process(ctx, msg) + } + for _, w := range o.workers { return w.Process(ctx, msg) } @@ -66,6 +80,32 @@ func (o *Orchestrator) handleTask(ctx context.Context, msg bus.Message) (bus.Mes return bus.Message{}, fmt.Errorf("orchestrator %s: no workers available", o.ID()) } +func containsIgnoreCase(s, substr string) bool { + if len(substr) > len(s) { + return false + } + for i := 0; i <= len(s)-len(substr); i++ { + match := true + for j := 0; j < len(substr); j++ { + if toLower(s[i+j]) != toLower(substr[j]) { + match = false + break + } + } + if match { + return true + } + } + return false +} + +func toLower(b byte) byte { + if b >= 'A' && b <= 'Z' { + return b + ('a' - 'A') + } + return b +} + // handleSystem processes internal system messages. func (o *Orchestrator) handleSystem(ctx context.Context, msg bus.Message) (bus.Message, error) { return bus.Message{ @@ -91,6 +131,12 @@ func (o *Orchestrator) RemoveWorker(id string) { delete(o.workers, id) } +func (o *Orchestrator) SetDefaultWorker(w Agent) { + o.mu.Lock() + defer o.mu.Unlock() + o.defaultWorker = w +} + // WorkerCount returns the number of registered workers. func (o *Orchestrator) WorkerCount() int { o.mu.RLock() diff --git a/pkg/actor/subagent.go b/pkg/actor/subagent.go new file mode 100644 index 0000000..dbb5e15 --- /dev/null +++ b/pkg/actor/subagent.go @@ -0,0 +1,149 @@ +package actor + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/orca/orca/pkg/bus" + "github.com/orca/orca/pkg/llm" +) + +type SubAgent struct { + *BaseAgent + llmBackend llm.LLM + systemPrompt string + role string + streamWriter io.Writer +} + +type SubAgentOption func(*SubAgent) + +func WithSubAgentSystemPrompt(prompt string) SubAgentOption { + return func(a *SubAgent) { + a.systemPrompt = prompt + } +} + +func WithSubAgentRole(role string) SubAgentOption { + return func(a *SubAgent) { + a.role = role + } +} + +func WithSubAgentStreamWriter(w io.Writer) SubAgentOption { + return func(a *SubAgent) { + a.streamWriter = w + } +} + +func NewSubAgent(id string, llmBackend llm.LLM, opts ...SubAgentOption) *SubAgent { + sa := &SubAgent{ + BaseAgent: NewBaseAgent(id, "subagent"), + llmBackend: llmBackend, + systemPrompt: "你是一个专业的AI助手。", + role: "assistant", + } + + for _, opt := range opts { + opt(sa) + } + + sa.SetHandler(sa.handleMessage) + + if err := sa.Start(); err != nil { + panic(fmt.Sprintf("subagent: failed to start %s: %v", id, err)) + } + + return sa +} + +func (sa *SubAgent) Role() string { + return sa.role +} + +func (sa *SubAgent) SystemPrompt() string { + return sa.systemPrompt +} + +func (sa *SubAgent) SetStreamWriter(w io.Writer) { + sa.streamWriter = w +} + +func (sa *SubAgent) handleMessage(ctx context.Context, msg bus.Message) (bus.Message, error) { + switch msg.Type { + case bus.MsgTypeTaskRequest: + return sa.handleTask(ctx, msg) + case bus.MsgTypeSystem: + return sa.handleSystem(ctx, msg) + default: + return bus.Message{}, fmt.Errorf("subagent %s: unsupported message type %s", sa.ID(), msg.Type) + } +} + +func (sa *SubAgent) handleTask(ctx context.Context, msg bus.Message) (bus.Message, error) { + messages := []llm.Message{ + { + Role: "system", + Content: sa.systemPrompt, + }, + { + Role: "user", + Content: fmt.Sprintf("%v", msg.Content), + }, + } + + content, err := sa.streamChat(ctx, messages) + if err != nil { + return bus.Message{}, fmt.Errorf("subagent %s: LLM call failed: %w", sa.ID(), err) + } + + return bus.Message{ + ID: msg.ID + "-response", + Type: bus.MsgTypeTaskResponse, + From: sa.ID(), + To: msg.From, + Content: content, + Metadata: map[string]string{ + "processed_by": sa.ID(), + "agent_role": sa.role, + }, + }, nil +} + +func (sa *SubAgent) streamChat(ctx context.Context, messages []llm.Message) (string, error) { + var content strings.Builder + + if sa.streamWriter != nil { + fmt.Fprintf(sa.streamWriter, "\n[%s] ", sa.ID()) + } + + err := sa.llmBackend.Stream(ctx, messages, func(chunk string) error { + content.WriteString(chunk) + if sa.streamWriter != nil { + fmt.Fprint(sa.streamWriter, chunk) + } + return nil + }) + + if err != nil { + return "", err + } + + if sa.streamWriter != nil { + fmt.Fprintln(sa.streamWriter) + } + + return content.String(), nil +} + +func (sa *SubAgent) handleSystem(ctx context.Context, msg bus.Message) (bus.Message, error) { + return bus.Message{ + ID: msg.ID + "-ack", + Type: bus.MsgTypeSystem, + From: sa.ID(), + To: msg.From, + Content: fmt.Sprintf("subagent %s acknowledged", sa.ID()), + }, nil +} diff --git a/pkg/actor/system.go b/pkg/actor/system.go index b448638..aa63851 100644 --- a/pkg/actor/system.go +++ b/pkg/actor/system.go @@ -1,3 +1,4 @@ +// Package actor 为 Orca 框架实现 Actor 模型。 package actor import ( @@ -8,55 +9,55 @@ import ( "github.com/orca/orca/pkg/tool" ) -// System manages the lifecycle of all agents in the Orca actor framework. +// System 管理 Orca Actor 框架中所有智能体的生命周期。 // -// It provides centralized agent creation, monitoring, and shutdown -// capabilities. Agents are identified by unique IDs and organized by role. +// 它提供集中式的智能体创建、监控和关闭功能。 +// 智能体通过唯一 ID 标识,并按角色组织。 type System struct { mu sync.RWMutex agents map[string]Agent nextID int64 } -// NewSystem creates a new empty actor System. +// NewSystem 创建一个新的空 Actor 系统。 func NewSystem() *System { return &System{ agents: make(map[string]Agent), } } -// AgentInfo holds summary information about a managed agent. +// AgentInfo 保存关于已管理智能体的摘要信息。 type AgentInfo struct { ID string `json:"id"` Role string `json:"role"` Status ActorStatus `json:"status"` } -// CreateOrchestrator creates a new Orchestrator agent and registers it. +// CreateOrchestrator 创建一个新的 Orchestrator 智能体并注册它。 func (s *System) CreateOrchestrator(bus interface{}) (*Orchestrator, error) { id := s.nextAgentID("orch") return s.addOrchestrator(id, bus) } -// CreateWorker creates a new Worker agent and registers it. +// CreateWorker 创建一个新的 Worker 智能体并注册它。 func (s *System) CreateWorker() (*Worker, error) { id := s.nextAgentID("worker") return s.addWorker(id) } -// CreateToolWorker creates a new ToolWorker agent with the given tool manager and registers it. +// CreateToolWorker 使用给定的工具管理器创建一个新的 ToolWorker 智能体并注册它。 func (s *System) CreateToolWorker(manager *tool.Manager) (*ToolWorker, error) { id := s.nextAgentID("tool") return s.addToolWorker(id, manager) } -// nextAgentID generates a unique agent ID with the given prefix. +// nextAgentID 使用给定前缀生成唯一的智能体 ID。 func (s *System) nextAgentID(prefix string) string { n := atomic.AddInt64(&s.nextID, 1) return fmt.Sprintf("%s-%d", prefix, n) } -// addOrchestrator creates and registers an orchestrator. +// addOrchestrator 创建并注册一个编排器。 func (s *System) addOrchestrator(id string, busInterface interface{}) (*Orchestrator, error) { mb, ok := busInterface.(interface{ Bus() }) var orch *Orchestrator @@ -73,7 +74,7 @@ func (s *System) addOrchestrator(id string, busInterface interface{}) (*Orchestr return orch, nil } -// addWorker creates and registers a worker. +// addWorker 创建并注册一个工作器。 func (s *System) addWorker(id string) (*Worker, error) { w := NewWorker(id) @@ -84,7 +85,7 @@ func (s *System) addWorker(id string) (*Worker, error) { return w, nil } -// addToolWorker creates and registers a tool worker with the given tool manager. +// addToolWorker 使用给定的工具管理器创建并注册一个工具工作器。 func (s *System) addToolWorker(id string, manager *tool.Manager) (*ToolWorker, error) { w := NewToolWorker(id, manager) @@ -95,7 +96,7 @@ func (s *System) addToolWorker(id string, manager *tool.Manager) (*ToolWorker, e return w, nil } -// StopAgent stops and removes a single agent by ID. +// StopAgent 通过 ID 停止并移除单个智能体。 func (s *System) StopAgent(id string) error { s.mu.Lock() agent, ok := s.agents[id] @@ -109,7 +110,7 @@ func (s *System) StopAgent(id string) error { return agent.Stop() } -// GetAgent retrieves a registered agent by ID. +// GetAgent 通过 ID 检索已注册的智能体。 func (s *System) GetAgent(id string) (Agent, bool) { s.mu.RLock() defer s.mu.RUnlock() @@ -117,7 +118,7 @@ func (s *System) GetAgent(id string) (Agent, bool) { return agent, ok } -// ListAgents returns all registered agents. +// ListAgents 返回所有已注册的智能体。 func (s *System) ListAgents() []Agent { s.mu.RLock() defer s.mu.RUnlock() @@ -129,7 +130,7 @@ func (s *System) ListAgents() []Agent { return agents } -// AgentInfos returns summary information for all registered agents. +// AgentInfos 返回所有已注册智能体的摘要信息。 func (s *System) AgentInfos() []AgentInfo { s.mu.RLock() defer s.mu.RUnlock() @@ -157,7 +158,7 @@ func (s *System) AgentInfos() []AgentInfo { return infos } -// StopAll gracefully stops all registered agents. +// StopAll 优雅地停止所有已注册的智能体。 func (s *System) StopAll() error { s.mu.Lock() defer s.mu.Unlock() @@ -172,7 +173,7 @@ func (s *System) StopAll() error { return lastErr } -// AgentCount returns the number of registered agents. +// AgentCount 返回已注册智能体的数量。 func (s *System) AgentCount() int { s.mu.RLock() defer s.mu.RUnlock() diff --git a/pkg/actor/worker.go b/pkg/actor/worker.go index 4a6815b..eb09969 100644 --- a/pkg/actor/worker.go +++ b/pkg/actor/worker.go @@ -1,3 +1,4 @@ +// Package actor 为 Orca 框架实现 Actor 模型。 package actor import ( @@ -7,17 +8,16 @@ import ( "github.com/orca/orca/pkg/bus" ) -// Worker is an agent that processes tasks and makes tool calls. +// Worker 是一个处理任务并进行工具调用的智能体。 // -// Workers are the execution units in the actor system. They receive -// task requests from the orchestrator, process them (potentially making -// tool calls), and return results. +// Worker 是 Actor 系统中的执行单元。它们接收来自编排器的 +// 任务请求,处理这些任务(可能需要进行工具调用),并返回结果。 type Worker struct { *BaseAgent } -// NewWorker creates a new Worker agent with the given id. -// The agent is started automatically upon creation. +// NewWorker 使用给定的 ID 创建一个新的 Worker 智能体。 +// 智能体在创建时会自动启动。 func NewWorker(id string) *Worker { w := &Worker{ BaseAgent: NewBaseAgent(id, "worker"), @@ -29,7 +29,7 @@ func NewWorker(id string) *Worker { return w } -// handleMessage routes incoming messages to the appropriate handler. +// handleMessage 将传入的消息路由到适当的处理程序。 func (w *Worker) handleMessage(ctx context.Context, msg bus.Message) (bus.Message, error) { switch msg.Type { case bus.MsgTypeTaskRequest: @@ -43,7 +43,7 @@ func (w *Worker) handleMessage(ctx context.Context, msg bus.Message) (bus.Messag } } -// handleTask processes a task request and returns a task response. +// handleTask 处理任务请求并返回任务响应。 func (w *Worker) handleTask(ctx context.Context, msg bus.Message) (bus.Message, error) { // Process the task - in a real implementation this would involve // the LLM, tool calls, etc. @@ -59,8 +59,7 @@ func (w *Worker) handleTask(ctx context.Context, msg bus.Message) (bus.Message, }, nil } -// handleToolCall processes a tool call request, transitions to WaitingForTool -// state, and returns the result. +// handleToolCall 处理工具调用请求,转换到 WaitingForTool 状态,并返回结果。 func (w *Worker) handleToolCall(ctx context.Context, msg bus.Message) (bus.Message, error) { w.setStatus(StatusWaitingForTool) defer w.setStatus(StatusProcessing) @@ -76,7 +75,7 @@ func (w *Worker) handleToolCall(ctx context.Context, msg bus.Message) (bus.Messa }, nil } -// handleSystem processes internal system messages. +// handleSystem 处理内部系统消息。 func (w *Worker) handleSystem(ctx context.Context, msg bus.Message) (bus.Message, error) { return bus.Message{ ID: msg.ID + "-ack", diff --git a/pkg/kernel/kernel.go b/pkg/kernel/kernel.go index e9660e7..8393a4f 100644 --- a/pkg/kernel/kernel.go +++ b/pkg/kernel/kernel.go @@ -1,7 +1,7 @@ -// Package kernel implements the microkernel core of the Orca framework. +// Package kernel 实现了 Orca 框架的微内核核心。 // -// The kernel is the minimal runtime that manages plugin lifecycle, -// message routing, and inter-component communication. +// 内核是一个最小化运行时,负责管理插件生命周期、 +// 消息路由和组件间通信。 package kernel import ( @@ -10,8 +10,9 @@ import ( "io" "log" "os" + "path/filepath" + "strings" "sync" - "time" "github.com/orca/orca/internal/config" "github.com/orca/orca/pkg/actor" @@ -23,16 +24,16 @@ import ( "github.com/orca/orca/pkg/tool" ) -// Kernel is the microkernel core of the Orca framework. +// Kernel 是 Orca 框架的微内核核心。 // -// It orchestrates plugin lifecycle, message routing, and inter-component -// communication. The kernel initializes and manages: -// - Message bus for inter-component communication -// - Plugin registry for extensibility -// - Session manager for conversation persistence -// - Tool manager with built-in tools -// - Skill manager for skill-based automation -// - Actor system with orchestrator, workers, and LLM agent +// 它编排插件生命周期、消息路由和组件间通信。 +// 内核初始化并管理以下组件: +// - 消息总线,用于组件间通信 +// - 插件注册表,支持扩展 +// - 会话管理器,用于对话持久化 +// - 工具管理器,包含内置工具 +// - 技能管理器,用于基于技能的自动化 +// - Actor 系统,包含编排器、工作者和 LLM 代理 type Kernel struct { mu sync.RWMutex mb bus.MessageBus @@ -49,14 +50,21 @@ type Kernel struct { orch *actor.Orchestrator llmAgent *actor.LLMAgent toolWorker *actor.ToolWorker + subAgents map[string]actor.Agent } -// New creates a new Kernel instance with default configuration. +// New 从配置文件创建一个新的 Kernel 实例。 +// 默认加载 ./config.toml 或 ~/.orca/config.toml。 func New() *Kernel { - return NewWithConfig(config.DefaultConfig()) + cfg, err := config.LoadConfig() + if err != nil { + log.Printf("kernel: warning: failed to load config: %v, using defaults", err) + cfg = config.DefaultConfig() + } + return NewWithConfig(cfg) } -// NewWithConfig creates a new Kernel instance with the given configuration. +// NewWithConfig 使用给定的配置创建一个新的 Kernel 实例。 func NewWithConfig(cfg *config.Config) *Kernel { if cfg == nil { cfg = config.DefaultConfig() @@ -67,6 +75,7 @@ func NewWithConfig(cfg *config.Config) *Kernel { registry: plugin.NewRegistry(), config: cfg, actorSystem: actor.NewSystem(), + subAgents: make(map[string]actor.Agent), } // Initialize session manager @@ -90,7 +99,7 @@ func NewWithConfig(cfg *config.Config) *Kernel { return k } -// registerBuiltinTools registers all built-in tools with the tool manager. +// registerBuiltinTools 向工具管理器注册所有内置工具。 func (k *Kernel) registerBuiltinTools() { tools := []tool.Tool{ tool.NewExecTool(nil), // exec - shell commands @@ -107,9 +116,8 @@ func (k *Kernel) registerBuiltinTools() { } } -// initializeActorSystem sets up the orchestrator, tool worker, and LLM agent. +// initializeActorSystem 设置编排器、工具工作者和 LLM 代理。 func (k *Kernel) initializeActorSystem() { - // Create orchestrator orch, err := k.actorSystem.CreateOrchestrator(k) if err != nil { log.Printf("kernel: warning: failed to create orchestrator: %v", err) @@ -117,7 +125,6 @@ func (k *Kernel) initializeActorSystem() { } k.orch = orch - // Create tool worker tw, err := k.actorSystem.CreateToolWorker(k.toolMgr) if err != nil { log.Printf("kernel: warning: failed to create tool worker: %v", err) @@ -125,10 +132,15 @@ func (k *Kernel) initializeActorSystem() { } k.toolWorker = tw - // Create LLM backend ollama := k.createLLMBackend() - // Create LLM agent + k.createSubAgents(ollama) + + agentCallTool := tool.NewAgentCallTool(k.findAgent) + if err := k.toolMgr.Register(agentCallTool); err != nil { + log.Printf("kernel: warning: failed to register agent_call tool: %v", err) + } + llmAgentID := fmt.Sprintf("llm-%d", len(k.actorSystem.ListAgents())+1) llmOpts := []actor.LLMAgentOption{ actor.WithToolManager(k.toolMgr), @@ -136,6 +148,14 @@ func (k *Kernel) initializeActorSystem() { actor.WithWindowSize(k.config.Session.MaxHistory), } + if prompt := k.config.GetSystemPrompt(); prompt != "" { + llmOpts = append(llmOpts, actor.WithSystemPrompt(prompt)) + } + + if k.skillMgr != nil { + llmOpts = append(llmOpts, actor.WithSkillManager(k.skillMgr)) + } + if k.sessionMgr != nil { sessionID := "default" if _, err := k.sessionMgr.GetSession(sessionID); err != nil { @@ -150,14 +170,88 @@ func (k *Kernel) initializeActorSystem() { ) } + if len(k.subAgents) > 0 { + agentDescs := make(map[string]string) + for name, agent := range k.subAgents { + if sa, ok := agent.(*actor.SubAgent); ok { + agentDescs[name] = sa.SystemPrompt() + } + } + llmOpts = append(llmOpts, actor.WithSubAgents(agentDescs)) + } + llmAgent := actor.NewLLMAgent(llmAgentID, ollama, llmOpts...) k.llmAgent = llmAgent - // Register LLM agent as orchestrator's worker k.orch.AddWorker(llmAgent) - - // Also register tool worker as a fallback worker k.orch.AddWorker(tw) + k.orch.SetDefaultWorker(llmAgent) +} + +func (k *Kernel) createSubAgents(llmBackend llm.LLM) { + promptDir := expandHomeDir("~/.orca/prompts") + entries, err := os.ReadDir(promptDir) + if err != nil { + log.Printf("kernel: warning: cannot read prompts dir %s: %v", promptDir, err) + return + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(name, ".md") { + continue + } + if name == "assistant.md" { + continue + } + + agentName := strings.TrimSuffix(name, ".md") + promptPath := filepath.Join(promptDir, name) + content, err := os.ReadFile(promptPath) + if err != nil { + log.Printf("kernel: warning: failed to read prompt %s: %v", promptPath, err) + continue + } + + prompt := string(content) + if strings.TrimSpace(prompt) == "" { + log.Printf("kernel: warning: empty prompt file %s, skipping", promptPath) + continue + } + + agent := actor.NewSubAgent(agentName, llmBackend, + actor.WithSubAgentRole(agentName), + actor.WithSubAgentSystemPrompt(prompt), + ) + k.subAgents[agentName] = agent + k.orch.AddWorker(agent) + log.Printf("kernel: created sub-agent %q from %s", agentName, promptPath) + } + + log.Printf("kernel: created %d sub-agents", len(k.subAgents)) +} + +func expandHomeDir(path string) string { + if len(path) > 0 && path[0] == '~' { + home, err := os.UserHomeDir() + if err == nil { + return home + path[1:] + } + } + return path +} + +func (k *Kernel) findAgent(name string) (tool.Agent, bool) { + k.mu.RLock() + defer k.mu.RUnlock() + agent, ok := k.subAgents[name] + if !ok { + return nil, false + } + return agent.(tool.Agent), true } func (k *Kernel) createLLMBackend() llm.LLM { @@ -170,123 +264,95 @@ func (k *Kernel) createLLMBackend() llm.LLM { } func (k *Kernel) createOllamaBackend() llm.LLM { - baseURL := k.config.Ollama.BaseURL - model := k.config.Ollama.Model - timeout := k.config.Ollama.Timeout - - if v := os.Getenv("OLLAMA_BASE_URL"); v != "" { - baseURL = v - } - if v := os.Getenv("OLLAMA_MODEL"); v != "" { - model = v - } - if v := os.Getenv("OLLAMA_TIMEOUT"); v != "" { - if d, err := time.ParseDuration(v); err == nil { - timeout = d - } - } + cfg := k.config.Ollama client := llm.NewOllamaClient( - llm.WithBaseURL(baseURL), - llm.WithModel(model), - llm.WithTimeout(timeout), + llm.WithBaseURL(cfg.BaseURL), + llm.WithModel(cfg.Model), + llm.WithTimeout(cfg.Timeout), ) - log.Printf("kernel: created Ollama client (model=%s, url=%s)", model, baseURL) + log.Printf("kernel: created Ollama client (model=%s, url=%s)", cfg.Model, cfg.BaseURL) return client } func (k *Kernel) createDeepSeekBackend() llm.LLM { - baseURL := k.config.DeepSeek.BaseURL - model := k.config.DeepSeek.Model - apiKey := k.config.DeepSeek.APIKey - timeout := k.config.DeepSeek.Timeout - - if v := os.Getenv("DEEPSEEK_BASE_URL"); v != "" { - baseURL = v - } - if v := os.Getenv("DEEPSEEK_MODEL"); v != "" { - model = v - } - if v := os.Getenv("DEEPSEEK_API_KEY"); v != "" { - apiKey = v - } - if v := os.Getenv("DEEPSEEK_TIMEOUT"); v != "" { - if d, err := time.ParseDuration(v); err == nil { - timeout = d - } - } + cfg := k.config.DeepSeek client := llm.NewDeepSeekClient( - llm.WithDeepSeekBaseURL(baseURL), - llm.WithDeepSeekModel(model), - llm.WithDeepSeekAPIKey(apiKey), - llm.WithDeepSeekTimeout(timeout), + llm.WithDeepSeekBaseURL(cfg.BaseURL), + llm.WithDeepSeekModel(cfg.Model), + llm.WithDeepSeekAPIKey(cfg.APIKey), + llm.WithDeepSeekTimeout(cfg.Timeout), ) - log.Printf("kernel: created DeepSeek client (model=%s)", model) + log.Printf("kernel: created DeepSeek client (model=%s)", cfg.Model) return client } -// Bus returns the kernel's message bus. +// Bus 返回内核的消息总线。 func (k *Kernel) Bus() bus.MessageBus { return k.mb } -// Registry returns the plugin registry. +// Registry 返回插件注册表。 func (k *Kernel) Registry() *plugin.Registry { return k.registry } -// SessionManager returns the session manager. +// SessionManager 返回会话管理器。 func (k *Kernel) SessionManager() *session.Manager { return k.sessionMgr } -// ToolManager returns the tool manager. +// ToolManager 返回工具管理器。 func (k *Kernel) ToolManager() *tool.Manager { return k.toolMgr } -// SkillManager returns the skill manager. +// SkillManager 返回技能管理器。 func (k *Kernel) SkillManager() *skill.Manager { return k.skillMgr } -// ActorSystem returns the actor system. +// ActorSystem 返回 Actor 系统。 func (k *Kernel) ActorSystem() *actor.System { return k.actorSystem } -// Orchestrator returns the orchestrator agent. +// Orchestrator 返回编排器代理。 func (k *Kernel) Orchestrator() *actor.Orchestrator { return k.orch } -// LLMAgent returns the LLM agent. +// LLMAgent 返回 LLM 代理。 func (k *Kernel) LLMAgent() *actor.LLMAgent { return k.llmAgent } -// SetStreamWriter sets the writer for streaming LLM output. +// SetStreamWriter 设置用于流式 LLM 输出的写入器。 func (k *Kernel) SetStreamWriter(w io.Writer) { if k.llmAgent != nil { k.llmAgent.SetStreamWriter(w) } + for _, agent := range k.subAgents { + if sa, ok := agent.(*actor.SubAgent); ok { + sa.SetStreamWriter(w) + } + } } -// SendMessage sends a message from a source to the LLM agent. +// SendMessage 从发送方向 LLM 代理发送消息。 // -// This is the primary public API for interacting with the Orca system. -// It creates a task request message and sends it through the orchestrator -// to the LLM agent for processing. +// 这是与 Orca 系统交互的主要公共 API。 +// 它创建一个任务请求消息,并通过编排器发送给 LLM 代理处理。 // -// Parameters: -// - from: the sender identifier (e.g., "user", "cli") -// - to: the recipient (use "llm" for the LLM agent) -// - content: the message content (plain text) +// 参数: +// - from: 发送者标识(例如 "user"、"cli") +// - to: 接收者(使用 "llm" 表示 LLM 代理) +// - content: 消息内容(纯文本) // -// Returns the response content as a string, or an error. +// 返回响应内容的字符串,或错误。 func (k *Kernel) SendMessage(from, to, content string) (string, error) { if !k.IsRunning() { return "", fmt.Errorf("kernel: kernel is not running") @@ -320,7 +386,7 @@ func (k *Kernel) SendMessage(from, to, content string) (string, error) { } } -// InitPlugins loads and initializes skills from the skills directory. +// InitPlugins 从技能目录加载并初始化技能。 func (k *Kernel) InitPlugins() error { if k.skillMgr == nil { return nil @@ -336,17 +402,17 @@ func (k *Kernel) InitPlugins() error { return nil } -// GetPlugin returns a registered plugin by name. +// GetPlugin 根据名称返回已注册的插件。 func (k *Kernel) GetPlugin(name string) (plugin.Plugin, bool) { return k.registry.Get(name) } -// ListPlugins returns all currently registered plugins. +// ListPlugins 返回所有当前已注册的插件。 func (k *Kernel) ListPlugins() []plugin.Plugin { return k.registry.List() } -// RegisterPlugin registers a plugin without starting it. +// RegisterPlugin 注册一个插件但不启动它。 func (k *Kernel) RegisterPlugin(p plugin.Plugin) error { k.mu.Lock() defer k.mu.Unlock() @@ -358,7 +424,7 @@ func (k *Kernel) RegisterPlugin(p plugin.Plugin) error { return k.registry.Register(p) } -// UnregisterPlugin removes a plugin from the registry. +// UnregisterPlugin 从注册表中移除一个插件。 func (k *Kernel) UnregisterPlugin(name string) error { k.mu.Lock() defer k.mu.Unlock() @@ -366,7 +432,7 @@ func (k *Kernel) UnregisterPlugin(name string) error { return k.registry.Unregister(name) } -// Start initializes all registered plugins and marks the kernel as running. +// Start 初始化所有已注册的插件,并将内核标记为运行中。 func (k *Kernel) Start() error { k.mu.Lock() defer k.mu.Unlock() @@ -398,7 +464,7 @@ func (k *Kernel) Start() error { return nil } -// Stop gracefully shuts down the kernel. +// Stop 优雅地关闭内核。 func (k *Kernel) Stop() error { k.mu.Lock() defer k.mu.Unlock() @@ -431,7 +497,7 @@ func (k *Kernel) Stop() error { return k.mb.Close() } -// IsRunning returns whether the kernel has been started and not yet stopped. +// IsRunning 返回内核是否已启动且尚未停止。 func (k *Kernel) IsRunning() bool { k.mu.RLock() defer k.mu.RUnlock() diff --git a/pkg/plugin/registry.go b/pkg/plugin/registry.go index 69b2127..2f9bf53 100644 --- a/pkg/plugin/registry.go +++ b/pkg/plugin/registry.go @@ -1,3 +1,8 @@ +// Package plugin 定义 Orca 框架的插件系统。 +// +// 所有对框架的扩展(技能、工具、LLM 驱动等) +// 都作为实现 Plugin 接口的插件来实现。 +// 内核管理插件生命周期:加载、初始化、启动、停止、关闭。 package plugin import ( @@ -5,14 +10,14 @@ import ( "sync" ) -// Registry is a thread-safe map that manages plugin registration. +// Registry 是管理插件注册的线程安全映射。 type Registry struct { mu sync.RWMutex plugins map[string]Plugin states map[string]PluginState } -// NewRegistry creates a new empty plugin registry. +// NewRegistry 创建新的空插件注册表。 func NewRegistry() *Registry { return &Registry{ plugins: make(map[string]Plugin), @@ -20,7 +25,7 @@ func NewRegistry() *Registry { } } -// Register adds a plugin to the registry. +// Register 将插件添加到注册表。 func (r *Registry) Register(p Plugin) error { r.mu.Lock() defer r.mu.Unlock() @@ -35,7 +40,7 @@ func (r *Registry) Register(p Plugin) error { return nil } -// Unregister removes a plugin from the registry. +// Unregister 从注册表中移除插件。 func (r *Registry) Unregister(name string) error { r.mu.Lock() defer r.mu.Unlock() @@ -49,7 +54,7 @@ func (r *Registry) Unregister(name string) error { return nil } -// Get retrieves a plugin by name. +// Get 按名称检索插件。 func (r *Registry) Get(name string) (Plugin, bool) { r.mu.RLock() defer r.mu.RUnlock() @@ -58,7 +63,7 @@ func (r *Registry) Get(name string) (Plugin, bool) { return p, ok } -// List returns all registered plugins. +// List 返回所有已注册的插件。 func (r *Registry) List() []Plugin { r.mu.RLock() defer r.mu.RUnlock() @@ -70,7 +75,7 @@ func (r *Registry) List() []Plugin { return plugins } -// State returns the lifecycle state of a registered plugin. +// State 返回已注册插件的生命周期状态。 func (r *Registry) State(name string) PluginState { r.mu.RLock() defer r.mu.RUnlock() @@ -81,7 +86,7 @@ func (r *Registry) State(name string) PluginState { return StateUnknown } -// SetState updates the lifecycle state of a registered plugin. +// SetState 更新已注册插件的生命周期状态。 func (r *Registry) SetState(name string, state PluginState) { r.mu.Lock() defer r.mu.Unlock() @@ -91,7 +96,7 @@ func (r *Registry) SetState(name string, state PluginState) { } } -// Count returns the number of registered plugins. +// Count 返回已注册插件的数量。 func (r *Registry) Count() int { r.mu.RLock() defer r.mu.RUnlock() diff --git a/pkg/sandbox/process.go b/pkg/sandbox/process.go index 4d4d8f6..b3bcc68 100644 --- a/pkg/sandbox/process.go +++ b/pkg/sandbox/process.go @@ -11,8 +11,8 @@ import ( ) const ( - // DefaultOutputLimit is the maximum number of bytes captured from stdout/stderr (64 KB). - DefaultOutputLimit = 64 * 1024 + // DefaultOutputLimit is the maximum number of bytes captured from stdout/stderr (512 KB). + DefaultOutputLimit = 512 * 1024 // DefaultWorkingDir is the default working directory for sandboxed commands. DefaultWorkingDir = "." diff --git a/pkg/session/jsonl.go b/pkg/session/jsonl.go index d08a722..3304dec 100644 --- a/pkg/session/jsonl.go +++ b/pkg/session/jsonl.go @@ -1,3 +1,4 @@ +// Package session 为 Orca 框架提供对话会话管理功能。 package session import ( @@ -9,18 +10,18 @@ import ( "sync" ) -// JSONLStore implements the Store interface using JSONL files. +// JSONLStore 使用 JSONL 文件实现 Store 接口。 // -// Each session is stored in a separate file named {session_id}.jsonl -// under the configured storage directory. Every line in the file is a -// JSON-encoded SessionMessage. New messages are appended in O(1) time. +// 每个会话存储在单独的 {session_id}.jsonl 文件中, +// 位于配置的存储目录下。文件中的每一行都是一个 +// JSON 编码的 SessionMessage。新消息以 O(1) 时间追加。 type JSONLStore struct { storageDir string mu sync.RWMutex } -// NewJSONLStore creates a new JSONLStore with the given storage directory. -// The directory is created if it does not exist. +// NewJSONLStore 使用给定的存储目录创建新的 JSONLStore。 +// 如果目录不存在,则创建它。 func NewJSONLStore(storageDir string) (*JSONLStore, error) { if err := os.MkdirAll(storageDir, 0755); err != nil { return nil, fmt.Errorf("failed to create session storage directory %q: %w", storageDir, err) @@ -28,19 +29,19 @@ func NewJSONLStore(storageDir string) (*JSONLStore, error) { return &JSONLStore{storageDir: storageDir}, nil } -// path returns the full file path for the given session ID. +// path 返回给定会话 ID 的完整文件路径。 func (s *JSONLStore) path(sessionID string) string { return filepath.Join(s.storageDir, sessionID+".jsonl") } -// archivePath returns the archive file path for the given session ID. +// archivePath 返回给定会话 ID 的归档文件路径。 func (s *JSONLStore) archivePath(sessionID string) string { return filepath.Join(s.storageDir, sessionID+".jsonl.archived") } -// Save appends a message to a session's JSONL file. -// If the file does not exist, it is created. -// This is an O(1) append operation. +// Save 将消息追加到会话的 JSONL 文件。 +// 如果文件不存在,则创建它。 +// 这是一个 O(1) 追加操作。 func (s *JSONLStore) Save(sessionID string, msg SessionMessage) error { s.mu.Lock() defer s.mu.Unlock() @@ -63,8 +64,8 @@ func (s *JSONLStore) Save(sessionID string, msg SessionMessage) error { return nil } -// Load retrieves all messages for a session in chronological order. -// Returns an error if the session file does not exist. +// Load 按时间顺序检索会话的所有消息。 +// 如果会话文件不存在,则返回错误。 func (s *JSONLStore) Load(sessionID string) ([]SessionMessage, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -86,7 +87,7 @@ func (s *JSONLStore) Load(sessionID string) ([]SessionMessage, error) { return parseJSONL(data) } -// parseJSONL parses a JSONL byte slice into a slice of SessionMessage. +// parseJSONL 将 JSONL 字节切片解析为 SessionMessage 切片。 func parseJSONL(data []byte) ([]SessionMessage, error) { var messages []SessionMessage trimmed := strings.TrimSpace(string(data)) @@ -109,7 +110,7 @@ func parseJSONL(data []byte) ([]SessionMessage, error) { return messages, nil } -// List returns all session IDs by scanning the storage directory. +// List 通过扫描存储目录返回所有会话 ID。 func (s *JSONLStore) List() ([]string, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -129,7 +130,7 @@ func (s *JSONLStore) List() ([]string, error) { return sessions, nil } -// Exists checks whether a session file exists (active or archived). +// Exists 检查会话文件是否存在(活动或已归档)。 func (s *JSONLStore) Exists(sessionID string) (bool, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -150,7 +151,7 @@ func (s *JSONLStore) Exists(sessionID string) (bool, error) { return false, nil } -// Archive moves a session file to the archived state by renaming it. +// Archive 通过重命名将会话文件移动到归档状态。 func (s *JSONLStore) Archive(sessionID string) error { s.mu.Lock() defer s.mu.Unlock() @@ -164,7 +165,7 @@ func (s *JSONLStore) Archive(sessionID string) error { return nil } -// Delete permanently removes a session file and its archive. +// Delete 永久移除会话文件及其归档。 func (s *JSONLStore) Delete(sessionID string) error { s.mu.Lock() defer s.mu.Unlock() @@ -184,7 +185,7 @@ func (s *JSONLStore) Delete(sessionID string) error { return lastErr } -// StorageDir returns the storage directory path. +// StorageDir 返回存储目录路径。 func (s *JSONLStore) StorageDir() string { return s.storageDir } diff --git a/pkg/session/manager.go b/pkg/session/manager.go index 17b7db3..749b58c 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -1,3 +1,7 @@ +// Package session 为 Orca 框架提供对话会话管理功能。 +// +// 会话持久化对话历史记录,并为 LLM 交互提供基于上下文窗口的 +// 检索功能。默认存储后端使用 JSONL 文件,支持 O(1) 追加写入。 package session import ( @@ -8,10 +12,10 @@ import ( "github.com/orca/orca/pkg/bus" ) -// Manager provides high-level session lifecycle operations. +// Manager 提供高级会话生命周期操作。 // -// It wraps a Store with caching, context window management, and -// event publishing on the message bus. +// 它使用 Store 包装缓存、上下文窗口管理和 +// 消息总线上的事件发布。 type Manager struct { store Store bus bus.MessageBus @@ -19,7 +23,7 @@ type Manager struct { mu sync.RWMutex } -// NewManager creates a new session Manager with the given store and optional message bus. +// NewManager 使用给定的存储和可选消息总线创建一个新的会话管理器。 func NewManager(store Store, mb bus.MessageBus) *Manager { return &Manager{ store: store, @@ -28,7 +32,7 @@ func NewManager(store Store, mb bus.MessageBus) *Manager { } } -// CreateSession creates a new session with the given ID and optional metadata. +// CreateSession 使用给定的 ID 和可选元数据创建一个新会话。 func (m *Manager) CreateSession(id string, metadata map[string]string) (*Session, error) { m.mu.Lock() defer m.mu.Unlock() @@ -62,7 +66,7 @@ func (m *Manager) CreateSession(id string, metadata map[string]string) (*Session return session, nil } -// GetSession retrieves a session by ID, checking the cache and then the store. +// GetSession 通过 ID 检索会话,先检查缓存,然后检查存储。 func (m *Manager) GetSession(id string) (*Session, error) { m.mu.RLock() session, ok := m.cache[id] @@ -106,7 +110,7 @@ func (m *Manager) GetSession(id string) (*Session, error) { return session, nil } -// AddMessage appends a message to a session and persists it. +// AddMessage 将消息追加到会话并持久化。 func (m *Manager) AddMessage(sessionID string, role MessageRole, content string, metadata map[string]string) (*SessionMessage, error) { msg := SessionMessage{ Role: role, @@ -138,8 +142,8 @@ func (m *Manager) AddMessage(sessionID string, role MessageRole, content string, return &msg, nil } -// GetContext returns the most recent N messages in a session. -// If windowSize <= 0 or >= total messages, all messages are returned. +// GetContext 返回会话中最近的 N 条消息。 +// 如果 windowSize <= 0 或 >= 总消息数,则返回所有消息。 func (m *Manager) GetContext(sessionID string, windowSize int) ([]SessionMessage, error) { session, err := m.GetSession(sessionID) if err != nil { @@ -153,7 +157,7 @@ func (m *Manager) GetContext(sessionID string, windowSize int) ([]SessionMessage return messages, nil } -// ArchiveSession archives a session, making it read-only. +// ArchiveSession 归档会话,使其变为只读。 func (m *Manager) ArchiveSession(id string) error { m.mu.Lock() defer m.mu.Unlock() @@ -179,7 +183,7 @@ func (m *Manager) ArchiveSession(id string) error { return nil } -// DeleteSession permanently removes a session. +// DeleteSession 永久移除会话。 func (m *Manager) DeleteSession(id string) error { m.mu.Lock() delete(m.cache, id) @@ -187,12 +191,12 @@ func (m *Manager) DeleteSession(id string) error { return m.store.Delete(id) } -// ListSessions returns all known session IDs. +// ListSessions 返回所有已知的会话 ID。 func (m *Manager) ListSessions() ([]string, error) { return m.store.List() } -// Store returns the underlying Store. +// Store 返回底层存储。 func (m *Manager) Store() Store { return m.store } diff --git a/pkg/session/store.go b/pkg/session/store.go index c251edd..0d86415 100644 --- a/pkg/session/store.go +++ b/pkg/session/store.go @@ -1,28 +1,29 @@ +// Package session 为 Orca 框架提供对话会话管理功能。 package session -// Store defines the persistence interface for session message storage. +// Store 定义会话消息存储的持久化接口。 // -// Implementations must be safe for concurrent use. The default implementation -// uses JSONL files (one file per session) with O(1) append writes. +// 实现必须支持并发使用。默认实现使用 JSONL 文件 +//(每个会话一个文件),支持 O(1) 追加写入。 type Store interface { - // Save appends a single message to a session's history. - // Creates the session file if it does not exist. + // Save 将单条消息追加到会话历史中。 + // 如果会话文件不存在,则创建它。 Save(sessionID string, msg SessionMessage) error - // Load retrieves all messages for a session in chronological order. - // Returns an error if the session does not exist. + // Load 按时间顺序检索会话的所有消息。 + // 如果会话不存在,则返回错误。 Load(sessionID string) ([]SessionMessage, error) - // List returns all known session IDs. + // List 返回所有已知的会话 ID。 List() ([]string, error) - // Exists checks whether a session exists in the store. + // Exists 检查会话是否存在于存储中。 Exists(sessionID string) (bool, error) - // Archive marks a session as archived (read-only). - // This is a soft delete that preserves the data. + // Archive 将会话标记为已归档(只读)。 + // 这是一个保留数据的软删除操作。 Archive(sessionID string) error - // Delete removes a session permanently from the store. + // Delete 从存储中永久移除会话。 Delete(sessionID string) error } diff --git a/pkg/skill/manager.go b/pkg/skill/manager.go index 78e1158..caa388d 100644 --- a/pkg/skill/manager.go +++ b/pkg/skill/manager.go @@ -1,3 +1,9 @@ +// Package skill 提供 Skill 定义和管理系统。 +// +// 技能是从 ~/.agents/skills/ 加载的可组合能力。 +// 每个技能都有一个带 YAML 前置元数据的 SKILL.md 清单文件, +// 以及可选的 scripts/ 子目录中的脚本。 +// 技能可以通过触发关键词被发现和调用。 package skill import ( @@ -10,27 +16,25 @@ import ( ) const ( - // DefaultSkillsDir is the default directory for user-installed skills. + // DefaultSkillsDir 是用户安装技能的默认目录。 DefaultSkillsDir = "~/.agents/skills" - // SkillManifestFile is the name of the skill manifest file. + // SkillManifestFile 是技能清单文件的名称。 SkillManifestFile = "SKILL.md" ) -// Manager is a thread-safe registry for loading, storing, and querying Skills. +// Manager 是一个用于加载、存储和查询技能的线程安全注册表。 // -// Skills are loaded from a directory tree where each subdirectory containing -// a SKILL.md file is treated as a skill. The Manager automatically discovers -// skills on initialization and provides methods for finding skills by trigger -// keywords or by name. +// 技能从目录树加载,每个包含 SKILL.md 文件的子目录都被视为一个技能。 +// 管理器在初始化时自动发现技能,并提供按触发关键词或名称查找技能的方法。 type Manager struct { mu sync.RWMutex skillsDir string skills map[string]*Skill } -// NewManager creates a new Skill manager that scans the given directory for skills. -// If skillsDir is empty, DefaultSkillsDir is used. +// NewManager 创建一个新的技能管理器,扫描给定目录中的技能。 +// 如果 skillsDir 为空,则使用 DefaultSkillsDir。 func NewManager(skillsDir string) *Manager { if skillsDir == "" { skillsDir = DefaultSkillsDir @@ -44,8 +48,8 @@ func NewManager(skillsDir string) *Manager { } } -// LoadAll scans the skills directory and loads all skills found. -// It returns the number of skills loaded and any errors encountered. +// LoadAll 扫描技能目录并加载所有找到的技能。 +// 返回加载的技能数量和遇到的任何错误。 func (m *Manager) LoadAll() (int, error) { m.mu.Lock() defer m.mu.Unlock() @@ -104,7 +108,7 @@ func (m *Manager) LoadAll() (int, error) { return loaded, nil } -// GetSkill retrieves a skill by its name. Returns false if not found. +// GetSkill 按名称检索技能。如果未找到则返回 false。 func (m *Manager) GetSkill(name string) (*Skill, bool) { m.mu.RLock() defer m.mu.RUnlock() @@ -113,7 +117,7 @@ func (m *Manager) GetSkill(name string) (*Skill, bool) { return skill, ok } -// ListSkills returns all loaded skills sorted by name. +// ListSkills 返回按名称排序的所有已加载技能。 func (m *Manager) ListSkills() []*Skill { m.mu.RLock() defer m.mu.RUnlock() @@ -129,8 +133,8 @@ func (m *Manager) ListSkills() []*Skill { return result } -// FindSkill finds skills whose triggers match the given query string. -// Returns all matching skills sorted by relevance (more trigger matches first). +// FindSkill 查找触发器与给定查询字符串匹配的技能。 +// 返回所有匹配的技能,按相关性排序(触发器匹配多的优先)。 func (m *Manager) FindSkill(query string) []*Skill { m.mu.RLock() defer m.mu.RUnlock() @@ -150,17 +154,17 @@ func (m *Manager) FindSkill(query string) []*Skill { return matches } -// SkillsDir returns the directory being scanned for skills. +// SkillsDir 返回正在扫描技能的目录。 func (m *Manager) SkillsDir() string { return m.skillsDir } -// Reload refreshes all skills from disk. +// Reload 从磁盘刷新所有技能。 func (m *Manager) Reload() (int, error) { return m.LoadAll() } -// countMatches counts how many of the skill's triggers match the query. +// countMatches 计算技能的触发器中有多少个与查询匹配。 func countMatches(skill *Skill, query string) int { count := 0 queryLower := strings.ToLower(query) diff --git a/pkg/skill/parser.go b/pkg/skill/parser.go index 43913cb..70739ba 100644 --- a/pkg/skill/parser.go +++ b/pkg/skill/parser.go @@ -1,3 +1,4 @@ +// Package skill 提供 Skill 定义和管理系统。 package skill import ( @@ -8,14 +9,14 @@ import ( "strings" ) -// FrontmatterDelimiters for YAML frontmatter in SKILL.md files. +// frontmatterDelim 是 SKILL.md 文件中 YAML 前置元数据的分隔符。 const ( frontmatterDelim = "---" ) -// ParseSkillFile parses a SKILL.md file and returns a populated Skill struct. +// ParseSkillFile 解析 SKILL.md 文件并返回填充好的 Skill 结构体。 // -// The expected format is: +// 预期格式为: // // --- // name: my-skill @@ -35,8 +36,8 @@ func ParseSkillFile(path string) (*Skill, error) { return ParseSkillData(path, data) } -// ParseSkillData parses SKILL.md content from raw bytes. -// The path parameter is used to locate the scripts/ directory. +// ParseSkillData 从原始字节解析 SKILL.md 内容。 +// path 参数用于定位 scripts/ 目录。 func ParseSkillData(path string, data []byte) (*Skill, error) { content := string(data) @@ -74,8 +75,8 @@ func ParseSkillData(path string, data []byte) (*Skill, error) { return skill, nil } -// parseFrontmatter extracts YAML frontmatter delimited by "---" lines -// and populates the Skill struct fields. +// parseFrontmatter 提取由 "---" 行分隔的 YAML 前置元数据 +// 并填充 Skill 结构体字段。 func parseFrontmatter(content string, skill *Skill) (string, error) { content = strings.TrimSpace(content) @@ -108,8 +109,8 @@ func parseFrontmatter(content string, skill *Skill) (string, error) { return body, nil } -// parseSimpleYAML parses a simplified YAML format for skill frontmatter. -// Supports: string values, quoted strings, and array values. +// parseSimpleYAML 解析技能前置元数据的简化 YAML 格式。 +// 支持:字符串值、带引号的字符串和数组值。 func parseSimpleYAML(yaml string, skill *Skill) error { lines := strings.Split(yaml, "\n") for _, line := range lines { @@ -144,7 +145,7 @@ func parseSimpleYAML(yaml string, skill *Skill) error { return nil } -// parseYAMLArray parses a YAML array like '["a", "b", "c"]' or '[a, b, c]'. +// parseYAMLArray 解析 YAML 数组,如 '["a", "b", "c"]' 或 '[a, b, c]'。 func parseYAMLArray(value string) ([]string, error) { value = strings.TrimSpace(value) @@ -175,7 +176,7 @@ func parseYAMLArray(value string) ([]string, error) { return []string{}, nil } -// splitCommas splits a comma-separated string respecting quoted sections. +// splitCommas 分割逗号分隔的字符串,尊重引号部分。 func splitCommas(s string) []string { var parts []string var current strings.Builder @@ -206,7 +207,7 @@ func splitCommas(s string) []string { return parts } -// trimQuotes removes surrounding quotes from a string value. +// trimQuotes 移除字符串值周围的引号。 func trimQuotes(s string) string { s = strings.TrimSpace(s) if len(s) >= 2 { @@ -217,7 +218,7 @@ func trimQuotes(s string) string { return s } -// discoverScripts lists all executable/readable files in a scripts directory. +// discoverScripts 列出 scripts 目录中所有可执行/可读的文件。 func discoverScripts(scriptsDir string) ([]string, error) { entries, err := os.ReadDir(scriptsDir) if err != nil { @@ -236,7 +237,7 @@ func discoverScripts(scriptsDir string) ([]string, error) { return scripts, nil } -// LoadSkillFromDir loads a skill from a directory containing a SKILL.md file. +// LoadSkillFromDir 从包含 SKILL.md 文件的目录加载技能。 func LoadSkillFromDir(dir string) (*Skill, error) { skillPath := filepath.Join(dir, "SKILL.md") if _, err := os.Stat(skillPath); os.IsNotExist(err) { diff --git a/pkg/skill/skill.go b/pkg/skill/skill.go index 640ca9c..294ff8a 100644 --- a/pkg/skill/skill.go +++ b/pkg/skill/skill.go @@ -1,9 +1,9 @@ -// Package skill provides the Skill definition and management system. +// Package skill 提供 Skill 定义和管理系统。 // -// Skills are composable capabilities loaded from ~/.agents/skills/. -// Each skill has a SKILL.md manifest with YAML frontmatter and optional -// scripts in a scripts/ subdirectory. Skills can be discovered and -// invoked by trigger keywords. +// 技能是从 ~/.agents/skills/ 加载的可组合能力。 +// 每个技能都有一个带 YAML 前置元数据的 SKILL.md 清单文件, +// 以及可选的 scripts/ 子目录中的脚本。 +// 技能可以通过触发关键词被发现和调用。 package skill import ( @@ -11,30 +11,29 @@ import ( "strings" ) -// Skill represents a composable capability loaded from the skills directory. +// Skill 表示从技能目录加载的可组合能力。 // -// Each Skill is defined by a SKILL.md file with YAML frontmatter containing -// metadata (name, description, triggers) and optional executable scripts -// in a scripts/ subdirectory. +// 每个技能由 SKILL.md 文件定义,其中包含 YAML 前置元数据(名称、描述、触发器) +// 以及可选的 scripts/ 子目录中的可执行脚本。 type Skill struct { - // Name is the unique identifier for this skill (e.g., "dev-browser"). + // Name 是此技能的唯一标识符(例如 "dev-browser")。 Name string `yaml:"name"` - // Description is a human-readable explanation of what this skill does. + // Description 是对此技能功能的可读说明。 Description string `yaml:"description"` - // Triggers are keywords that activate this skill from natural language. + // Triggers 是从自然语言中激活此技能的关键词。 Triggers []string `yaml:"triggers"` - // Scripts is the list of script file names in the scripts/ directory. + // Scripts 是 scripts/ 目录中的脚本文件名称列表。 Scripts []string `yaml:"-"` - // ScriptsDir is the absolute path to the scripts/ directory. + // ScriptsDir 是 scripts/ 目录的绝对路径。 ScriptsDir string `yaml:"-"` - // Body is the markdown content after the YAML frontmatter. + // Body 是 YAML 前置元数据之后的 Markdown 内容。 Body string `yaml:"-"` - // Path is the absolute path to the SKILL.md file. + // Path 是 SKILL.md 文件的绝对路径。 Path string `yaml:"-"` } -// MatchTrigger checks if the given query matches any of the skill's triggers. -// Matching is case-insensitive and supports partial matches. +// MatchTrigger 检查给定查询是否与技能的任何触发器匹配。 +// 匹配不区分大小写,支持部分匹配。 func (s *Skill) MatchTrigger(query string) bool { query = strings.ToLower(query) for _, trigger := range s.Triggers { @@ -45,12 +44,12 @@ func (s *Skill) MatchTrigger(query string) bool { return false } -// String returns a human-readable representation of the skill. +// String 返回技能的可读表示形式。 func (s *Skill) String() string { return fmt.Sprintf("Skill{Name: %q, Triggers: %v, Scripts: %d}", s.Name, s.Triggers, len(s.Scripts)) } -// HasScripts returns true if the skill has at least one script. +// HasScripts 如果技能至少有一个脚本,则返回 true。 func (s *Skill) HasScripts() bool { return len(s.Scripts) > 0 } diff --git a/pkg/tool/agent_call.go b/pkg/tool/agent_call.go new file mode 100644 index 0000000..acac218 --- /dev/null +++ b/pkg/tool/agent_call.go @@ -0,0 +1,79 @@ +package tool + +import ( + "context" + "fmt" + + "github.com/orca/orca/pkg/bus" +) + +type Agent interface { + Process(ctx context.Context, msg bus.Message) (bus.Message, error) +} + +type agentCallTool struct { + agentRegistry func(string) (Agent, bool) +} + +func NewAgentCallTool(registry func(string) (Agent, bool)) Tool { + return &agentCallTool{agentRegistry: registry} +} + +func (t *agentCallTool) Name() string { return "agent_call" } + +func (t *agentCallTool) Description() string { + return "调用其他专业Agent来协助完成任务。当你需要特定领域的专业知识时,使用此工具委托给专门的Agent处理。" +} + +func (t *agentCallTool) Parameters() map[string]ParameterSchema { + return map[string]ParameterSchema{ + "agent": { + Type: "string", + Description: "要调用的Agent名称(如 coder, reviewer, designer)", + Required: true, + }, + "task": { + Type: "string", + Description: "要委托给该Agent处理的具体任务描述", + Required: true, + }, + } +} + +func (t *agentCallTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) { + agentName, ok := args["agent"].(string) + if !ok || agentName == "" { + return ErrorResult("'agent' argument is required and must be a string"), nil + } + + task, ok := args["task"].(string) + if !ok || task == "" { + return ErrorResult("'task' argument is required and must be a string"), nil + } + + agent, ok := t.agentRegistry(agentName) + if !ok { + return ErrorResult(fmt.Sprintf("agent '%s' not found", agentName)), nil + } + + msg := bus.Message{ + Type: bus.MsgTypeTaskRequest, + From: "llm", + To: agentName, + Content: task, + } + + resp, err := agent.Process(ctx, msg) + if err != nil { + return ErrorResult(fmt.Sprintf("agent '%s' execution failed: %v", agentName, err)), nil + } + + return &Result{ + Success: true, + Data: map[string]interface{}{ + "agent": agentName, + "result": resp.Content, + "metadata": resp.Metadata, + }, + }, nil +} diff --git a/pkg/tool/builtin.go b/pkg/tool/builtin.go index 863ea8d..7db919e 100644 --- a/pkg/tool/builtin.go +++ b/pkg/tool/builtin.go @@ -45,9 +45,9 @@ func (t *execTool) Parameters() map[string]ParameterSchema { }, "timeout": { Type: "number", - Description: "Timeout in seconds for the command execution (default: 30)", + Description: "Timeout in seconds for the command execution (default: 120)", Required: false, - Default: float64(30), + Default: float64(120), }, "workdir": { Type: "string", diff --git a/pkg/tool/manager.go b/pkg/tool/manager.go index 6c3abb9..8657538 100644 --- a/pkg/tool/manager.go +++ b/pkg/tool/manager.go @@ -1,3 +1,8 @@ +// Package tool 定义 Tool 接口和工具管理系统。 +// +// 工具是可以由智能体或 LLM 调用的原子能力。 +// 每个工具都有名称、描述、参数模式(用于 LLM 函数调用) +// 和执行实际工作的 Execute 方法。 package tool import ( @@ -7,25 +12,23 @@ import ( "sync" ) -// Manager is a thread-safe registry that manages tool registration and execution. +// Manager 是管理工具注册和执行的安全注册表。 // -// Tools are registered by name (case-sensitive) and can be discovered, -// listed, and invoked through the Manager. Duplicate registration returns -// an error. +// 工具按名称(区分大小写)注册,可以通过管理器发现、 +// 列出和调用。重复注册将返回错误。 type Manager struct { mu sync.RWMutex tools map[string]Tool } -// NewManager creates a new empty tool manager. +// NewManager 创建新的空工具管理器。 func NewManager() *Manager { return &Manager{ tools: make(map[string]Tool), } } -// Register adds a tool to the manager. Returns an error if a tool with the -// same name is already registered. +// Register 将工具添加到管理器。如果已注册了同名工具,则返回错误。 func (m *Manager) Register(tool Tool) error { m.mu.Lock() defer m.mu.Unlock() @@ -39,7 +42,7 @@ func (m *Manager) Register(tool Tool) error { return nil } -// Unregister removes a tool from the manager by name. +// Unregister 按名称从管理器中移除工具。 func (m *Manager) Unregister(name string) error { m.mu.Lock() defer m.mu.Unlock() @@ -52,7 +55,7 @@ func (m *Manager) Unregister(name string) error { return nil } -// Get retrieves a tool by name. Returns false if not found. +// Get 按名称检索工具。如果未找到则返回 false。 func (m *Manager) Get(name string) (Tool, bool) { m.mu.RLock() defer m.mu.RUnlock() @@ -61,7 +64,7 @@ func (m *Manager) Get(name string) (Tool, bool) { return t, ok } -// List returns all registered tools sorted by name. +// List 返回按名称排序的所有已注册工具。 func (m *Manager) List() []Tool { m.mu.RLock() defer m.mu.RUnlock() @@ -77,8 +80,8 @@ func (m *Manager) List() []Tool { return result } -// Execute looks up a tool by name and invokes it with the given arguments. -// Returns an error if the tool is not found. +// Execute 按名称查找工具并使用给定参数调用它。 +// 如果未找到工具则返回错误。 func (m *Manager) Execute(name string, ctx context.Context, args map[string]interface{}) (*Result, error) { tool, ok := m.Get(name) if !ok { @@ -87,14 +90,14 @@ func (m *Manager) Execute(name string, ctx context.Context, args map[string]inte return tool.Execute(ctx, args) } -// Count returns the number of registered tools. +// Count 返回已注册工具的数量。 func (m *Manager) Count() int { m.mu.RLock() defer m.mu.RUnlock() return len(m.tools) } -// Names returns the names of all registered tools sorted alphabetically. +// Names 返回按字母顺序排序的所有已注册工具名称。 func (m *Manager) Names() []string { m.mu.RLock() defer m.mu.RUnlock() diff --git a/thoughts/ledgers/CONTINUITY_ses_1fd1.md b/thoughts/ledgers/CONTINUITY_ses_1fd1.md new file mode 100644 index 0000000..88489b3 --- /dev/null +++ b/thoughts/ledgers/CONTINUITY_ses_1fd1.md @@ -0,0 +1,82 @@ +--- +session: ses_1fd1 +updated: 2026-05-08T14:24:46.168Z +--- + +# Session Summary + +## Goal +使 Orca Agent Framework 能够识别和调用 `~/.agents/skills` 目录下的 skills,同时集成 DeepSeek LLM 提供商支持。 + +## Constraints & Preferences +- 保持与现有 Ollama 集成的兼容性 +- 默认使用 DeepSeek (deepseek-v4-flash) +- Skills 目录: `~/.agents/skills` +- Prompt-based tool calling (兼容不支持原生 function calling 的模型) +- 流式输出支持 + +## Progress +### Done +- [x] 实现 DeepSeek 客户端 (`pkg/llm/deepseek.go`) - 支持 Chat 和 Stream API +- [x] 更新配置系统支持 Provider 字段 (ollama/deepseek) +- [x] 默认 Provider 改为 DeepSeek,默认模型 deepseek-v4-flash +- [x] 更新 Kernel 根据 Provider 创建对应 LLM 后端 +- [x] 更新 CLI 支持 DeepSeek 环境变量 (DEEPSEEK_API_KEY, DEEPSEEK_MODEL 等) +- [x] 添加流式输出支持 (streamWriter, streamChat/syncChat) +- [x] 修复 sandbox 默认工作目录为当前目录 (`.` 而非 `/tmp/orca/sandbox`) +- [x] 修复 skills 加载路径为 `~/.agents/skills` +- [x] 在 `main.go` 中添加 `k.InitPlugins()` 调用以加载 skills +- [x] 在 LLMAgent 中添加 skillManager 字段和 WithSkillManager 选项 +- [x] 将 skill 信息注入系统提示 (buildSystemPrompt) +- [x] 所有 11 个包测试通过 + +### In Progress +- [ ] Skill 调用机制优化 - LLM 识别到 skill 后需要正确使用 skill 知识回答问题,而不是用 exec 工具查找 skill 目录 + +### Blocked +- (none) + +## Key Decisions +- **默认 Provider 使用 DeepSeek**: 用户明确要求,API key 已嵌入配置 +- **Prompt-based tool calling**: gemma4:e4b 不支持原生 function calling,需要通过系统提示注入工具描述,LLM 输出 JSON 格式调用工具 +- **Skill 信息注入系统提示**: 将可用 skills 的名称、描述、触发词加入系统提示,让 LLM 自主判断何时使用 +- **Sandbox 工作目录改为 `.`**: 避免文件创建在 `/tmp/orca/sandbox` 隔离目录中 + +## Next Steps +1. 修复 skill 调用机制 - LLM 应直接使用 skill 描述中的专业知识回答问题,而非用工具查找 skill 文件 +2. 考虑将 skill 的 body/markdown 内容也注入到对话中,或作为上下文参考 +3. 测试具体 skill 触发(如 ui-ux-pro-max 响应"设计网站"类请求) +4. 提交更改到 git dev 分支 +5. 推送代码到远程仓库 (需要 GitHub 认证) + +## Critical Context +- `~/.agents/skills` 下有 8 个已加载 skills: dev-browser, find-skills, md2pdf, project-memory, shadcn, skill-creator, ui-ux-pro-max, weapp-tailwindcss-guide +- 1 个 skill 加载错误: art-design-pro (缺少 frontmatter 中的 'name') +- DeepSeek API key: `sk-2f1049148e06492dbc304ba49c81c321` (已嵌入默认配置) +- 系统提示中 tools 和 skills 信息同时存在,LLM 可能混淆使用方式 +- 当前测试中 LLM 用 exec 工具查找 skill 目录,说明 skill 调用指导不够明确 + +## File Operations +### Read +- `/Users/wang/agent_dev/orca.ai/cmd/orca/main.go` +- `/Users/wang/agent_dev/orca.ai/internal/config/config.go` +- `/Users/wang/agent_dev/orca.ai/internal/config/config_test.go` +- `/Users/wang/agent_dev/orca.ai/pkg/actor/llm_agent.go` +- `/Users/wang/agent_dev/orca.ai/pkg/kernel/kernel.go` +- `/Users/wang/agent_dev/orca.ai/pkg/llm/deepseek.go` +- `/Users/wang/agent_dev/orca.ai/pkg/llm/llm.go` +- `/Users/wang/agent_dev/orca.ai/pkg/llm/types.go` +- `/Users/wang/agent_dev/orca.ai/pkg/sandbox/process.go` +- `/Users/wang/agent_dev/orca.ai/pkg/sandbox/sandbox.go` +- `/Users/wang/agent_dev/orca.ai/pkg/skill/manager.go` +- `/Users/wang/agent_dev/orca.ai/pkg/skill/skill.go` +- `/Users/wang/agent_dev/orca.ai/pkg/tool/builtin.go` + +### Modified +- `/Users/wang/agent_dev/orca.ai/cmd/orca/main.go` +- `/Users/wang/agent_dev/orca.ai/internal/config/config.go` +- `/Users/wang/agent_dev/orca.ai/internal/config/config_test.go` +- `/Users/wang/agent_dev/orca.ai/pkg/actor/llm_agent.go` +- `/Users/wang/agent_dev/orca.ai/pkg/kernel/kernel.go` +- `/Users/wang/agent_dev/orca.ai/pkg/llm/deepseek.go` +- `/Users/wang/agent_dev/orca.ai/pkg/sandbox/process.go`