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
This commit is contained in:
大森 2026-05-10 14:28:17 +08:00
parent 81f6802f2e
commit e18dde7c15
26 changed files with 1647 additions and 569 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
orca
test.pdf
~/
.memory/
config.toml.example
prompts/

View File

@ -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 package main
import ( import (
"bufio"
"fmt" "fmt"
"log" "log"
"os" "os"
"os/signal"
"strings"
"syscall"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/orca/orca/internal/config" "github.com/orca/orca/internal/config"
"github.com/orca/orca/internal/tui"
"github.com/orca/orca/pkg/kernel" "github.com/orca/orca/pkg/kernel"
) )
func main() { func main() {
// Load configuration from environment variables cfg, err := config.LoadConfig()
cfg := config.LoadConfigFromEnv() if err != nil {
log.Fatalf("Failed to load config: %v", err)
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
}
} }
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) k := kernel.NewWithConfig(cfg)
if err := k.Start(); err != nil { if err := k.Start(); err != nil {
log.Fatalf("Failed to start kernel: %v", err) log.Fatalf("Failed to start kernel: %v", err)
} }
@ -61,169 +26,15 @@ func main() {
log.Printf("Warning: failed to load skills: %v", err) 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") if _, err := p.Run(); err != nil {
fmt.Println("Kernel started successfully") fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err)
if cfg.Provider == config.ProviderDeepSeek { os.Exit(1)
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 := k.Stop(); err != nil { if err := k.Stop(); err != nil {
log.Fatalf("Failed to stop kernel: %v", err) log.Printf("Warning: error stopping 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.")
} }
} }

40
go.mod
View File

@ -1,3 +1,43 @@
module github.com/orca/orca module github.com/orca/orca
go 1.26.1 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
)

91
go.sum Normal file
View File

@ -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=

View File

@ -1,131 +1,174 @@
// Package config 为 Orca 框架提供配置管理功能。
//
// 配置从 TOML 文件加载,默认路径为 ~/.orca/config.toml。
// 所有模型参数和 Agent 身份描述均通过配置文件管理,
// 不再从环境变量读取。
package config package config
import ( import (
"fmt"
"os" "os"
"strconv" "path/filepath"
"time" "time"
"github.com/BurntSushi/toml"
) )
const ( const (
// ProviderOllama 选择本地 Ollama LLM 后端。
ProviderOllama = "ollama" ProviderOllama = "ollama"
// ProviderDeepSeek 选择云端 DeepSeek LLM 后端。
ProviderDeepSeek = "deepseek" ProviderDeepSeek = "deepseek"
) )
// Config 保存 Orca 框架的所有配置信息。
type Config struct { type Config struct {
Provider string `json:"provider"` Provider string `toml:"provider"`
Ollama OllamaConfig `json:"ollama"` Ollama OllamaConfig `toml:"ollama"`
DeepSeek DeepSeekConfig `json:"deepseek"` DeepSeek DeepSeekConfig `toml:"deepseek"`
Sandbox SandboxConfig `json:"sandbox"` Sandbox SandboxConfig `toml:"sandbox"`
Session SessionConfig `json:"session"` Session SessionConfig `toml:"session"`
Agent AgentConfig `toml:"agent"`
} }
// OllamaConfig 保存 Ollama LLM 后端的设置。
type OllamaConfig struct { type OllamaConfig struct {
BaseURL string `json:"base_url"` BaseURL string `toml:"base_url"`
Model string `json:"model"` Model string `toml:"model"`
Timeout time.Duration `json:"timeout"` Timeout time.Duration `toml:"timeout"`
} }
// DeepSeekConfig 保存 DeepSeek LLM 后端的设置。
type DeepSeekConfig struct { type DeepSeekConfig struct {
BaseURL string `json:"base_url"` BaseURL string `toml:"base_url"`
Model string `json:"model"` Model string `toml:"model"`
APIKey string `json:"api_key"` APIKey string `toml:"api_key"`
Timeout time.Duration `json:"timeout"` Timeout time.Duration `toml:"timeout"`
} }
// SandboxConfig 保存命令执行沙箱的设置。
type SandboxConfig struct { type SandboxConfig struct {
Timeout time.Duration `json:"timeout"` Timeout time.Duration `toml:"timeout"`
MaxMemory int64 `json:"max_memory"` MaxMemory int64 `toml:"max_memory"`
WorkingDir string `json:"working_dir"` WorkingDir string `toml:"working_dir"`
} }
// SessionConfig 保存对话会话存储的设置。
type SessionConfig struct { type SessionConfig struct {
StorageDir string `json:"storage_dir"` StorageDir string `toml:"storage_dir"`
MaxHistory int `json:"max_history"` 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 { func DefaultConfig() *Config {
home, _ := os.UserHomeDir()
return &Config{ return &Config{
Provider: ProviderDeepSeek, Provider: ProviderDeepSeek,
Ollama: OllamaConfig{ Ollama: OllamaConfig{
BaseURL: "http://localhost:11434", BaseURL: "http://localhost:11434",
Model: "gemma4:e4b", Model: "",
Timeout: 120 * time.Second, Timeout: 120 * time.Second,
}, },
DeepSeek: DeepSeekConfig{ DeepSeek: DeepSeekConfig{
BaseURL: "https://api.deepseek.com/v1", BaseURL: "https://api.deepseek.com/v1",
Model: "deepseek-v4-flash", Model: "",
APIKey: "sk-2f1049148e06492dbc304ba49c81c321", APIKey: "",
Timeout: 120 * time.Second, Timeout: 120 * time.Second,
}, },
Sandbox: SandboxConfig{ Sandbox: SandboxConfig{
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
MaxMemory: 512 * 1024 * 1024, MaxMemory: 512 * 1024 * 1024,
WorkingDir: "/tmp/orca/sandbox", WorkingDir: filepath.Join(home, ".orca", "sandbox"),
}, },
Session: SessionConfig{ Session: SessionConfig{
StorageDir: func() string { StorageDir: filepath.Join(home, ".orca", "sessions"),
home, _ := os.UserHomeDir()
return home + "/.orca/sessions"
}(),
MaxHistory: 100, MaxHistory: 100,
}, },
Agent: AgentConfig{
Role: "assistant",
SystemPrompt: "",
PromptFile: "",
},
} }
} }
func LoadConfigFromEnv() *Config { // LoadConfigFromFile 从指定路径加载 TOML 配置文件。
// 如果文件不存在,返回默认配置。
func LoadConfigFromFile(path string) (*Config, error) {
cfg := DefaultConfig() cfg := DefaultConfig()
if v := os.Getenv("ORCA_PROVIDER"); v != "" { if _, err := os.Stat(path); os.IsNotExist(err) {
cfg.Provider = v 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 data, err := os.ReadFile(path); err == nil {
} return string(data)
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
} }
} }
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 { func (c *Config) IsValid() error {

330
internal/tui/model.go Normal file
View File

@ -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())
}

73
internal/tui/styles.go Normal file
View File

@ -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)
)

51
internal/tui/writer.go Normal file
View File

@ -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):
}
}

View File

@ -6,10 +6,12 @@ import (
"fmt" "fmt"
"io" "io"
"strings" "strings"
"sync"
"github.com/orca/orca/pkg/bus" "github.com/orca/orca/pkg/bus"
"github.com/orca/orca/pkg/llm" "github.com/orca/orca/pkg/llm"
"github.com/orca/orca/pkg/session" "github.com/orca/orca/pkg/session"
"github.com/orca/orca/pkg/skill"
"github.com/orca/orca/pkg/tool" "github.com/orca/orca/pkg/tool"
) )
@ -25,9 +27,12 @@ type LLMAgent struct {
sessionMgr *session.Manager sessionMgr *session.Manager
sessionID string sessionID string
toolManager *tool.Manager toolManager *tool.Manager
skillManager *skill.Manager
toolWorker *ToolWorker toolWorker *ToolWorker
windowSize int windowSize int
streamWriter io.Writer streamWriter io.Writer
systemPrompt string
subAgents map[string]string
} }
// LLMAgentOption is a functional option for configuring the LLMAgent. // 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. // WithStreamWriter sets the writer for streaming LLM output.
func WithStreamWriter(w io.Writer) LLMAgentOption { func WithStreamWriter(w io.Writer) LLMAgentOption {
return func(a *LLMAgent) { 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. // NewLLMAgent creates a new LLMAgent with the given LLM backend and options.
// The agent is started automatically upon creation. // The agent is started automatically upon creation.
func NewLLMAgent(id string, backend llm.LLM, opts ...LLMAgentOption) *LLMAgent { 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) a.sessionMgr.AddMessage(a.sessionID, session.RoleUser, content, nil)
} }
// Build LLM messages from session context
llmMessages := a.buildLLMMessages() 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) // Call LLM (potentially multiple rounds for tool calls)
finalResponse, err := a.chatWithToolLoop(ctx, llmMessages) finalResponse, err := a.chatWithToolLoop(ctx, llmMessages)
if err != nil { if err != nil {
@ -165,10 +199,20 @@ func (a *LLMAgent) handleUserMessage(ctx context.Context, msg bus.Message) (bus.
func (a *LLMAgent) buildLLMMessages() []llm.Message { func (a *LLMAgent) buildLLMMessages() []llm.Message {
messages := make([]llm.Message, 0) messages := make([]llm.Message, 0)
if a.toolManager != nil { // 1. 用户自定义 system prompt配置式身份描述
if a.systemPrompt != "" {
messages = append(messages, llm.Message{ messages = append(messages, llm.Message{
Role: "system", Role: "system",
Content: a.buildToolSystemPrompt(), Content: a.systemPrompt,
})
}
// 2. 运行时工具说明(动态生成)
toolPrompt := a.buildToolPrompt()
if toolPrompt != "" {
messages = append(messages, llm.Message{
Role: "system",
Content: toolPrompt,
}) })
} }
@ -195,16 +239,13 @@ func (a *LLMAgent) buildLLMMessages() []llm.Message {
return messages return messages
} }
// buildToolSystemPrompt creates a system prompt describing all available tools. // buildToolPrompt 生成工具说明提示词(不包含身份描述)。
// This enables prompt-based tool calling for models without native function // 将可用工具和调用规则注入给 LLM支持基于提示词的工具调用。
// calling support. func (a *LLMAgent) buildToolPrompt() string {
func (a *LLMAgent) buildToolSystemPrompt() string {
if a.toolManager == nil {
return ""
}
var b strings.Builder var b strings.Builder
b.WriteString("你是一个 AI 助手,可以使用以下工具来完成用户的请求。\n\n")
if a.toolManager != nil {
b.WriteString("你可以使用以下工具来完成用户的请求。\n\n")
b.WriteString("可用工具列表:\n") b.WriteString("可用工具列表:\n")
for _, t := range a.toolManager.List() { for _, t := range a.toolManager.List() {
@ -217,9 +258,50 @@ func (a *LLMAgent) buildToolSystemPrompt() string {
b.WriteString("\n规则\n") b.WriteString("\n规则\n")
b.WriteString("1. 当你需要调用工具时,请在回复中**只输出**以下 JSON 格式(不要添加其他文字):\n") b.WriteString("1. 当你需要调用工具时,请在回复中**只输出**以下 JSON 格式(不要添加其他文字):\n")
b.WriteString(` {"tool": "工具名", "arguments": {"参数名": "参数值"}}` + "\n") b.WriteString(` {"tool": "工具名", "arguments": {"参数名": "参数值"}}` + "\n")
b.WriteString("2. 如果你已经看到了工具返回的结果,请直接根据结果回答用户,不要再次调用工具。\n") b.WriteString("2. 如果需要同时调用多个工具(并行执行),请输出 JSON 数组格式:\n")
b.WriteString("3. 如果你不需要调用工具,请直接回复用户。\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_callJSON数组格式让它们并行执行。\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 ""
}
return b.String() return b.String()
} }
@ -249,11 +331,11 @@ func (a *LLMAgent) chatWithToolLoop(ctx context.Context, messages []llm.Message)
Content: content, Content: content,
}) })
for _, tc := range toolCalls { results := a.executeToolCallsParallel(ctx, toolCalls)
resultContent := a.executeToolCall(ctx, tc) for _, result := range results {
messages = append(messages, llm.Message{ messages = append(messages, llm.Message{
Role: "user", Role: "user",
Content: fmt.Sprintf("工具 %s 的执行结果:%s", tc.Function.Name, resultContent), Content: result,
}) })
} }
} }
@ -298,14 +380,39 @@ func (a *LLMAgent) streamChat(ctx context.Context, messages []llm.Message) (stri
} }
func (a *LLMAgent) parseToolCallsFromContent(content string) []llm.ToolCall { 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 { var parsed struct {
Tool string `json:"tool"` Tool string `json:"tool"`
Arguments map[string]interface{} `json:"arguments"` Arguments map[string]interface{} `json:"arguments"`
} }
if err := json.Unmarshal([]byte(content), &parsed); err != nil || parsed.Tool == "" { if err := json.Unmarshal([]byte(cleanContent), &parsed); err == nil && parsed.Tool != "" {
return nil
}
argsJSON, _ := json.Marshal(parsed.Arguments) argsJSON, _ := json.Marshal(parsed.Arguments)
return []llm.ToolCall{{ return []llm.ToolCall{{
ID: "call_0", ID: "call_0",
@ -315,13 +422,62 @@ func (a *LLMAgent) parseToolCallsFromContent(content string) []llm.ToolCall {
Arguments: string(argsJSON), 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 { func (a *LLMAgent) executeToolCall(ctx context.Context, tc llm.ToolCall) string {
toolName := tc.Function.Name toolName := tc.Function.Name
// Parse arguments if a.streamWriter != nil {
fmt.Fprintf(a.streamWriter, "\n[正在执行工具: %s...]\n", toolName)
}
var args map[string]interface{} var args map[string]interface{}
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil { if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
args = map[string]interface{}{ 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 { if a.toolWorker != nil {
// Create a tool call message for the ToolWorker
toolCallMsg := bus.Message{ toolCallMsg := bus.Message{
ID: tc.ID, ID: tc.ID,
Type: bus.MsgTypeToolCall, 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) resultMsg, err := a.toolWorker.Process(ctx, toolCallMsg)
if a.streamWriter != nil {
fmt.Fprintf(a.streamWriter, "[工具 %s 执行完成]\n", toolName)
}
if err != nil { if err != nil {
return fmt.Sprintf(`{"error": "tool execution failed: %v"}`, err) return fmt.Sprintf(`{"error": "tool execution failed: %v"}`, err)
} }
// Serialize the result
resultJSON, err := json.Marshal(resultMsg.Content) resultJSON, err := json.Marshal(resultMsg.Content)
if err != nil { if err != nil {
return fmt.Sprintf(`{"error": "failed to marshal result: %v"}`, err) 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) return string(resultJSON)
} }
// Fallback: execute directly via tool.Manager
if a.toolManager != nil { if a.toolManager != nil {
result, err := a.toolManager.Execute(toolName, ctx, args) result, err := a.toolManager.Execute(toolName, ctx, args)
if a.streamWriter != nil {
fmt.Fprintf(a.streamWriter, "[工具 %s 执行完成]\n", toolName)
}
if err != nil { if err != nil {
return fmt.Sprintf(`{"error": "tool execution failed: %v"}`, err) 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) 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. // handleSystem processes internal system messages.
func (a *LLMAgent) handleSystem(ctx context.Context, msg bus.Message) (bus.Message, error) { func (a *LLMAgent) handleSystem(ctx context.Context, msg bus.Message) (bus.Message, error) {
return bus.Message{ return bus.Message{

View File

@ -16,6 +16,7 @@ import (
type Orchestrator struct { type Orchestrator struct {
*BaseAgent *BaseAgent
workers map[string]Agent workers map[string]Agent
defaultWorker Agent
bus bus.MessageBus bus bus.MessageBus
mu sync.RWMutex mu sync.RWMutex
} }
@ -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) { func (o *Orchestrator) handleTask(ctx context.Context, msg bus.Message) (bus.Message, error) {
o.mu.RLock() o.mu.RLock()
defer o.mu.RUnlock() 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()) 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 { for _, w := range o.workers {
return w.Process(ctx, msg) 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()) 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. // handleSystem processes internal system messages.
func (o *Orchestrator) handleSystem(ctx context.Context, msg bus.Message) (bus.Message, error) { func (o *Orchestrator) handleSystem(ctx context.Context, msg bus.Message) (bus.Message, error) {
return bus.Message{ return bus.Message{
@ -91,6 +131,12 @@ func (o *Orchestrator) RemoveWorker(id string) {
delete(o.workers, id) 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. // WorkerCount returns the number of registered workers.
func (o *Orchestrator) WorkerCount() int { func (o *Orchestrator) WorkerCount() int {
o.mu.RLock() o.mu.RLock()

149
pkg/actor/subagent.go Normal file
View File

@ -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
}

View File

@ -1,3 +1,4 @@
// Package actor 为 Orca 框架实现 Actor 模型。
package actor package actor
import ( import (
@ -8,55 +9,55 @@ import (
"github.com/orca/orca/pkg/tool" "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 { type System struct {
mu sync.RWMutex mu sync.RWMutex
agents map[string]Agent agents map[string]Agent
nextID int64 nextID int64
} }
// NewSystem creates a new empty actor System. // NewSystem 创建一个新的空 Actor 系统。
func NewSystem() *System { func NewSystem() *System {
return &System{ return &System{
agents: make(map[string]Agent), agents: make(map[string]Agent),
} }
} }
// AgentInfo holds summary information about a managed agent. // AgentInfo 保存关于已管理智能体的摘要信息。
type AgentInfo struct { type AgentInfo struct {
ID string `json:"id"` ID string `json:"id"`
Role string `json:"role"` Role string `json:"role"`
Status ActorStatus `json:"status"` Status ActorStatus `json:"status"`
} }
// CreateOrchestrator creates a new Orchestrator agent and registers it. // CreateOrchestrator 创建一个新的 Orchestrator 智能体并注册它。
func (s *System) CreateOrchestrator(bus interface{}) (*Orchestrator, error) { func (s *System) CreateOrchestrator(bus interface{}) (*Orchestrator, error) {
id := s.nextAgentID("orch") id := s.nextAgentID("orch")
return s.addOrchestrator(id, bus) return s.addOrchestrator(id, bus)
} }
// CreateWorker creates a new Worker agent and registers it. // CreateWorker 创建一个新的 Worker 智能体并注册它。
func (s *System) CreateWorker() (*Worker, error) { func (s *System) CreateWorker() (*Worker, error) {
id := s.nextAgentID("worker") id := s.nextAgentID("worker")
return s.addWorker(id) 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) { func (s *System) CreateToolWorker(manager *tool.Manager) (*ToolWorker, error) {
id := s.nextAgentID("tool") id := s.nextAgentID("tool")
return s.addToolWorker(id, manager) return s.addToolWorker(id, manager)
} }
// nextAgentID generates a unique agent ID with the given prefix. // nextAgentID 使用给定前缀生成唯一的智能体 ID。
func (s *System) nextAgentID(prefix string) string { func (s *System) nextAgentID(prefix string) string {
n := atomic.AddInt64(&s.nextID, 1) n := atomic.AddInt64(&s.nextID, 1)
return fmt.Sprintf("%s-%d", prefix, n) return fmt.Sprintf("%s-%d", prefix, n)
} }
// addOrchestrator creates and registers an orchestrator. // addOrchestrator 创建并注册一个编排器。
func (s *System) addOrchestrator(id string, busInterface interface{}) (*Orchestrator, error) { func (s *System) addOrchestrator(id string, busInterface interface{}) (*Orchestrator, error) {
mb, ok := busInterface.(interface{ Bus() }) mb, ok := busInterface.(interface{ Bus() })
var orch *Orchestrator var orch *Orchestrator
@ -73,7 +74,7 @@ func (s *System) addOrchestrator(id string, busInterface interface{}) (*Orchestr
return orch, nil return orch, nil
} }
// addWorker creates and registers a worker. // addWorker 创建并注册一个工作器。
func (s *System) addWorker(id string) (*Worker, error) { func (s *System) addWorker(id string) (*Worker, error) {
w := NewWorker(id) w := NewWorker(id)
@ -84,7 +85,7 @@ func (s *System) addWorker(id string) (*Worker, error) {
return w, nil 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) { func (s *System) addToolWorker(id string, manager *tool.Manager) (*ToolWorker, error) {
w := NewToolWorker(id, manager) w := NewToolWorker(id, manager)
@ -95,7 +96,7 @@ func (s *System) addToolWorker(id string, manager *tool.Manager) (*ToolWorker, e
return w, nil return w, nil
} }
// StopAgent stops and removes a single agent by ID. // StopAgent 通过 ID 停止并移除单个智能体。
func (s *System) StopAgent(id string) error { func (s *System) StopAgent(id string) error {
s.mu.Lock() s.mu.Lock()
agent, ok := s.agents[id] agent, ok := s.agents[id]
@ -109,7 +110,7 @@ func (s *System) StopAgent(id string) error {
return agent.Stop() return agent.Stop()
} }
// GetAgent retrieves a registered agent by ID. // GetAgent 通过 ID 检索已注册的智能体。
func (s *System) GetAgent(id string) (Agent, bool) { func (s *System) GetAgent(id string) (Agent, bool) {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
@ -117,7 +118,7 @@ func (s *System) GetAgent(id string) (Agent, bool) {
return agent, ok return agent, ok
} }
// ListAgents returns all registered agents. // ListAgents 返回所有已注册的智能体。
func (s *System) ListAgents() []Agent { func (s *System) ListAgents() []Agent {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
@ -129,7 +130,7 @@ func (s *System) ListAgents() []Agent {
return agents return agents
} }
// AgentInfos returns summary information for all registered agents. // AgentInfos 返回所有已注册智能体的摘要信息。
func (s *System) AgentInfos() []AgentInfo { func (s *System) AgentInfos() []AgentInfo {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
@ -157,7 +158,7 @@ func (s *System) AgentInfos() []AgentInfo {
return infos return infos
} }
// StopAll gracefully stops all registered agents. // StopAll 优雅地停止所有已注册的智能体。
func (s *System) StopAll() error { func (s *System) StopAll() error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -172,7 +173,7 @@ func (s *System) StopAll() error {
return lastErr return lastErr
} }
// AgentCount returns the number of registered agents. // AgentCount 返回已注册智能体的数量。
func (s *System) AgentCount() int { func (s *System) AgentCount() int {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()

View File

@ -1,3 +1,4 @@
// Package actor 为 Orca 框架实现 Actor 模型。
package actor package actor
import ( import (
@ -7,17 +8,16 @@ import (
"github.com/orca/orca/pkg/bus" "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 // Worker 是 Actor 系统中的执行单元。它们接收来自编排器的
// task requests from the orchestrator, process them (potentially making // 任务请求,处理这些任务(可能需要进行工具调用),并返回结果。
// tool calls), and return results.
type Worker struct { type Worker struct {
*BaseAgent *BaseAgent
} }
// NewWorker creates a new Worker agent with the given id. // NewWorker 使用给定的 ID 创建一个新的 Worker 智能体。
// The agent is started automatically upon creation. // 智能体在创建时会自动启动。
func NewWorker(id string) *Worker { func NewWorker(id string) *Worker {
w := &Worker{ w := &Worker{
BaseAgent: NewBaseAgent(id, "worker"), BaseAgent: NewBaseAgent(id, "worker"),
@ -29,7 +29,7 @@ func NewWorker(id string) *Worker {
return w return w
} }
// handleMessage routes incoming messages to the appropriate handler. // handleMessage 将传入的消息路由到适当的处理程序。
func (w *Worker) handleMessage(ctx context.Context, msg bus.Message) (bus.Message, error) { func (w *Worker) handleMessage(ctx context.Context, msg bus.Message) (bus.Message, error) {
switch msg.Type { switch msg.Type {
case bus.MsgTypeTaskRequest: 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) { func (w *Worker) handleTask(ctx context.Context, msg bus.Message) (bus.Message, error) {
// Process the task - in a real implementation this would involve // Process the task - in a real implementation this would involve
// the LLM, tool calls, etc. // the LLM, tool calls, etc.
@ -59,8 +59,7 @@ func (w *Worker) handleTask(ctx context.Context, msg bus.Message) (bus.Message,
}, nil }, nil
} }
// handleToolCall processes a tool call request, transitions to WaitingForTool // handleToolCall 处理工具调用请求,转换到 WaitingForTool 状态,并返回结果。
// state, and returns the result.
func (w *Worker) handleToolCall(ctx context.Context, msg bus.Message) (bus.Message, error) { func (w *Worker) handleToolCall(ctx context.Context, msg bus.Message) (bus.Message, error) {
w.setStatus(StatusWaitingForTool) w.setStatus(StatusWaitingForTool)
defer w.setStatus(StatusProcessing) defer w.setStatus(StatusProcessing)
@ -76,7 +75,7 @@ func (w *Worker) handleToolCall(ctx context.Context, msg bus.Message) (bus.Messa
}, nil }, nil
} }
// handleSystem processes internal system messages. // handleSystem 处理内部系统消息。
func (w *Worker) handleSystem(ctx context.Context, msg bus.Message) (bus.Message, error) { func (w *Worker) handleSystem(ctx context.Context, msg bus.Message) (bus.Message, error) {
return bus.Message{ return bus.Message{
ID: msg.ID + "-ack", ID: msg.ID + "-ack",

View File

@ -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 package kernel
import ( import (
@ -10,8 +10,9 @@ import (
"io" "io"
"log" "log"
"os" "os"
"path/filepath"
"strings"
"sync" "sync"
"time"
"github.com/orca/orca/internal/config" "github.com/orca/orca/internal/config"
"github.com/orca/orca/pkg/actor" "github.com/orca/orca/pkg/actor"
@ -23,16 +24,16 @@ import (
"github.com/orca/orca/pkg/tool" "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 { type Kernel struct {
mu sync.RWMutex mu sync.RWMutex
mb bus.MessageBus mb bus.MessageBus
@ -49,14 +50,21 @@ type Kernel struct {
orch *actor.Orchestrator orch *actor.Orchestrator
llmAgent *actor.LLMAgent llmAgent *actor.LLMAgent
toolWorker *actor.ToolWorker 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 { 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 { func NewWithConfig(cfg *config.Config) *Kernel {
if cfg == nil { if cfg == nil {
cfg = config.DefaultConfig() cfg = config.DefaultConfig()
@ -67,6 +75,7 @@ func NewWithConfig(cfg *config.Config) *Kernel {
registry: plugin.NewRegistry(), registry: plugin.NewRegistry(),
config: cfg, config: cfg,
actorSystem: actor.NewSystem(), actorSystem: actor.NewSystem(),
subAgents: make(map[string]actor.Agent),
} }
// Initialize session manager // Initialize session manager
@ -90,7 +99,7 @@ func NewWithConfig(cfg *config.Config) *Kernel {
return k return k
} }
// registerBuiltinTools registers all built-in tools with the tool manager. // registerBuiltinTools 向工具管理器注册所有内置工具。
func (k *Kernel) registerBuiltinTools() { func (k *Kernel) registerBuiltinTools() {
tools := []tool.Tool{ tools := []tool.Tool{
tool.NewExecTool(nil), // exec - shell commands 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() { func (k *Kernel) initializeActorSystem() {
// Create orchestrator
orch, err := k.actorSystem.CreateOrchestrator(k) orch, err := k.actorSystem.CreateOrchestrator(k)
if err != nil { if err != nil {
log.Printf("kernel: warning: failed to create orchestrator: %v", err) log.Printf("kernel: warning: failed to create orchestrator: %v", err)
@ -117,7 +125,6 @@ func (k *Kernel) initializeActorSystem() {
} }
k.orch = orch k.orch = orch
// Create tool worker
tw, err := k.actorSystem.CreateToolWorker(k.toolMgr) tw, err := k.actorSystem.CreateToolWorker(k.toolMgr)
if err != nil { if err != nil {
log.Printf("kernel: warning: failed to create tool worker: %v", err) log.Printf("kernel: warning: failed to create tool worker: %v", err)
@ -125,10 +132,15 @@ func (k *Kernel) initializeActorSystem() {
} }
k.toolWorker = tw k.toolWorker = tw
// Create LLM backend
ollama := k.createLLMBackend() 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) llmAgentID := fmt.Sprintf("llm-%d", len(k.actorSystem.ListAgents())+1)
llmOpts := []actor.LLMAgentOption{ llmOpts := []actor.LLMAgentOption{
actor.WithToolManager(k.toolMgr), actor.WithToolManager(k.toolMgr),
@ -136,6 +148,14 @@ func (k *Kernel) initializeActorSystem() {
actor.WithWindowSize(k.config.Session.MaxHistory), 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 { if k.sessionMgr != nil {
sessionID := "default" sessionID := "default"
if _, err := k.sessionMgr.GetSession(sessionID); err != nil { 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...) llmAgent := actor.NewLLMAgent(llmAgentID, ollama, llmOpts...)
k.llmAgent = llmAgent k.llmAgent = llmAgent
// Register LLM agent as orchestrator's worker
k.orch.AddWorker(llmAgent) k.orch.AddWorker(llmAgent)
// Also register tool worker as a fallback worker
k.orch.AddWorker(tw) 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 { func (k *Kernel) createLLMBackend() llm.LLM {
@ -170,123 +264,95 @@ func (k *Kernel) createLLMBackend() llm.LLM {
} }
func (k *Kernel) createOllamaBackend() llm.LLM { func (k *Kernel) createOllamaBackend() llm.LLM {
baseURL := k.config.Ollama.BaseURL cfg := k.config.Ollama
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
}
}
client := llm.NewOllamaClient( client := llm.NewOllamaClient(
llm.WithBaseURL(baseURL), llm.WithBaseURL(cfg.BaseURL),
llm.WithModel(model), llm.WithModel(cfg.Model),
llm.WithTimeout(timeout), 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 return client
} }
func (k *Kernel) createDeepSeekBackend() llm.LLM { func (k *Kernel) createDeepSeekBackend() llm.LLM {
baseURL := k.config.DeepSeek.BaseURL cfg := k.config.DeepSeek
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
}
}
client := llm.NewDeepSeekClient( client := llm.NewDeepSeekClient(
llm.WithDeepSeekBaseURL(baseURL), llm.WithDeepSeekBaseURL(cfg.BaseURL),
llm.WithDeepSeekModel(model), llm.WithDeepSeekModel(cfg.Model),
llm.WithDeepSeekAPIKey(apiKey), llm.WithDeepSeekAPIKey(cfg.APIKey),
llm.WithDeepSeekTimeout(timeout), 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 return client
} }
// Bus returns the kernel's message bus. // Bus 返回内核的消息总线。
func (k *Kernel) Bus() bus.MessageBus { func (k *Kernel) Bus() bus.MessageBus {
return k.mb return k.mb
} }
// Registry returns the plugin registry. // Registry 返回插件注册表。
func (k *Kernel) Registry() *plugin.Registry { func (k *Kernel) Registry() *plugin.Registry {
return k.registry return k.registry
} }
// SessionManager returns the session manager. // SessionManager 返回会话管理器。
func (k *Kernel) SessionManager() *session.Manager { func (k *Kernel) SessionManager() *session.Manager {
return k.sessionMgr return k.sessionMgr
} }
// ToolManager returns the tool manager. // ToolManager 返回工具管理器。
func (k *Kernel) ToolManager() *tool.Manager { func (k *Kernel) ToolManager() *tool.Manager {
return k.toolMgr return k.toolMgr
} }
// SkillManager returns the skill manager. // SkillManager 返回技能管理器。
func (k *Kernel) SkillManager() *skill.Manager { func (k *Kernel) SkillManager() *skill.Manager {
return k.skillMgr return k.skillMgr
} }
// ActorSystem returns the actor system. // ActorSystem 返回 Actor 系统。
func (k *Kernel) ActorSystem() *actor.System { func (k *Kernel) ActorSystem() *actor.System {
return k.actorSystem return k.actorSystem
} }
// Orchestrator returns the orchestrator agent. // Orchestrator 返回编排器代理。
func (k *Kernel) Orchestrator() *actor.Orchestrator { func (k *Kernel) Orchestrator() *actor.Orchestrator {
return k.orch return k.orch
} }
// LLMAgent returns the LLM agent. // LLMAgent 返回 LLM 代理。
func (k *Kernel) LLMAgent() *actor.LLMAgent { func (k *Kernel) LLMAgent() *actor.LLMAgent {
return k.llmAgent return k.llmAgent
} }
// SetStreamWriter sets the writer for streaming LLM output. // SetStreamWriter 设置用于流式 LLM 输出的写入器。
func (k *Kernel) SetStreamWriter(w io.Writer) { func (k *Kernel) SetStreamWriter(w io.Writer) {
if k.llmAgent != nil { if k.llmAgent != nil {
k.llmAgent.SetStreamWriter(w) 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. // 这是与 Orca 系统交互的主要公共 API。
// It creates a task request message and sends it through the orchestrator // 它创建一个任务请求消息,并通过编排器发送给 LLM 代理处理。
// to the LLM agent for processing.
// //
// Parameters: // 参数:
// - from: the sender identifier (e.g., "user", "cli") // - from: 发送者标识(例如 "user"、"cli"
// - to: the recipient (use "llm" for the LLM agent) // - to: 接收者(使用 "llm" 表示 LLM 代理)
// - content: the message content (plain text) // - content: 消息内容(纯文本)
// //
// Returns the response content as a string, or an error. // 返回响应内容的字符串,或错误。
func (k *Kernel) SendMessage(from, to, content string) (string, error) { func (k *Kernel) SendMessage(from, to, content string) (string, error) {
if !k.IsRunning() { if !k.IsRunning() {
return "", fmt.Errorf("kernel: kernel is not running") 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 { func (k *Kernel) InitPlugins() error {
if k.skillMgr == nil { if k.skillMgr == nil {
return nil return nil
@ -336,17 +402,17 @@ func (k *Kernel) InitPlugins() error {
return nil return nil
} }
// GetPlugin returns a registered plugin by name. // GetPlugin 根据名称返回已注册的插件。
func (k *Kernel) GetPlugin(name string) (plugin.Plugin, bool) { func (k *Kernel) GetPlugin(name string) (plugin.Plugin, bool) {
return k.registry.Get(name) return k.registry.Get(name)
} }
// ListPlugins returns all currently registered plugins. // ListPlugins 返回所有当前已注册的插件。
func (k *Kernel) ListPlugins() []plugin.Plugin { func (k *Kernel) ListPlugins() []plugin.Plugin {
return k.registry.List() return k.registry.List()
} }
// RegisterPlugin registers a plugin without starting it. // RegisterPlugin 注册一个插件但不启动它。
func (k *Kernel) RegisterPlugin(p plugin.Plugin) error { func (k *Kernel) RegisterPlugin(p plugin.Plugin) error {
k.mu.Lock() k.mu.Lock()
defer k.mu.Unlock() defer k.mu.Unlock()
@ -358,7 +424,7 @@ func (k *Kernel) RegisterPlugin(p plugin.Plugin) error {
return k.registry.Register(p) return k.registry.Register(p)
} }
// UnregisterPlugin removes a plugin from the registry. // UnregisterPlugin 从注册表中移除一个插件。
func (k *Kernel) UnregisterPlugin(name string) error { func (k *Kernel) UnregisterPlugin(name string) error {
k.mu.Lock() k.mu.Lock()
defer k.mu.Unlock() defer k.mu.Unlock()
@ -366,7 +432,7 @@ func (k *Kernel) UnregisterPlugin(name string) error {
return k.registry.Unregister(name) return k.registry.Unregister(name)
} }
// Start initializes all registered plugins and marks the kernel as running. // Start 初始化所有已注册的插件,并将内核标记为运行中。
func (k *Kernel) Start() error { func (k *Kernel) Start() error {
k.mu.Lock() k.mu.Lock()
defer k.mu.Unlock() defer k.mu.Unlock()
@ -398,7 +464,7 @@ func (k *Kernel) Start() error {
return nil return nil
} }
// Stop gracefully shuts down the kernel. // Stop 优雅地关闭内核。
func (k *Kernel) Stop() error { func (k *Kernel) Stop() error {
k.mu.Lock() k.mu.Lock()
defer k.mu.Unlock() defer k.mu.Unlock()
@ -431,7 +497,7 @@ func (k *Kernel) Stop() error {
return k.mb.Close() return k.mb.Close()
} }
// IsRunning returns whether the kernel has been started and not yet stopped. // IsRunning 返回内核是否已启动且尚未停止。
func (k *Kernel) IsRunning() bool { func (k *Kernel) IsRunning() bool {
k.mu.RLock() k.mu.RLock()
defer k.mu.RUnlock() defer k.mu.RUnlock()

View File

@ -1,3 +1,8 @@
// Package plugin 定义 Orca 框架的插件系统。
//
// 所有对框架的扩展技能、工具、LLM 驱动等)
// 都作为实现 Plugin 接口的插件来实现。
// 内核管理插件生命周期:加载、初始化、启动、停止、关闭。
package plugin package plugin
import ( import (
@ -5,14 +10,14 @@ import (
"sync" "sync"
) )
// Registry is a thread-safe map that manages plugin registration. // Registry 是管理插件注册的线程安全映射。
type Registry struct { type Registry struct {
mu sync.RWMutex mu sync.RWMutex
plugins map[string]Plugin plugins map[string]Plugin
states map[string]PluginState states map[string]PluginState
} }
// NewRegistry creates a new empty plugin registry. // NewRegistry 创建新的空插件注册表。
func NewRegistry() *Registry { func NewRegistry() *Registry {
return &Registry{ return &Registry{
plugins: make(map[string]Plugin), 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 { func (r *Registry) Register(p Plugin) error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
@ -35,7 +40,7 @@ func (r *Registry) Register(p Plugin) error {
return nil return nil
} }
// Unregister removes a plugin from the registry. // Unregister 从注册表中移除插件。
func (r *Registry) Unregister(name string) error { func (r *Registry) Unregister(name string) error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
@ -49,7 +54,7 @@ func (r *Registry) Unregister(name string) error {
return nil return nil
} }
// Get retrieves a plugin by name. // Get 按名称检索插件。
func (r *Registry) Get(name string) (Plugin, bool) { func (r *Registry) Get(name string) (Plugin, bool) {
r.mu.RLock() r.mu.RLock()
defer r.mu.RUnlock() defer r.mu.RUnlock()
@ -58,7 +63,7 @@ func (r *Registry) Get(name string) (Plugin, bool) {
return p, ok return p, ok
} }
// List returns all registered plugins. // List 返回所有已注册的插件。
func (r *Registry) List() []Plugin { func (r *Registry) List() []Plugin {
r.mu.RLock() r.mu.RLock()
defer r.mu.RUnlock() defer r.mu.RUnlock()
@ -70,7 +75,7 @@ func (r *Registry) List() []Plugin {
return plugins return plugins
} }
// State returns the lifecycle state of a registered plugin. // State 返回已注册插件的生命周期状态。
func (r *Registry) State(name string) PluginState { func (r *Registry) State(name string) PluginState {
r.mu.RLock() r.mu.RLock()
defer r.mu.RUnlock() defer r.mu.RUnlock()
@ -81,7 +86,7 @@ func (r *Registry) State(name string) PluginState {
return StateUnknown return StateUnknown
} }
// SetState updates the lifecycle state of a registered plugin. // SetState 更新已注册插件的生命周期状态。
func (r *Registry) SetState(name string, state PluginState) { func (r *Registry) SetState(name string, state PluginState) {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() 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 { func (r *Registry) Count() int {
r.mu.RLock() r.mu.RLock()
defer r.mu.RUnlock() defer r.mu.RUnlock()

View File

@ -11,8 +11,8 @@ import (
) )
const ( const (
// DefaultOutputLimit is the maximum number of bytes captured from stdout/stderr (64 KB). // DefaultOutputLimit is the maximum number of bytes captured from stdout/stderr (512 KB).
DefaultOutputLimit = 64 * 1024 DefaultOutputLimit = 512 * 1024
// DefaultWorkingDir is the default working directory for sandboxed commands. // DefaultWorkingDir is the default working directory for sandboxed commands.
DefaultWorkingDir = "." DefaultWorkingDir = "."

View File

@ -1,3 +1,4 @@
// Package session 为 Orca 框架提供对话会话管理功能。
package session package session
import ( import (
@ -9,18 +10,18 @@ import (
"sync" "sync"
) )
// JSONLStore implements the Store interface using JSONL files. // JSONLStore 使用 JSONL 文件实现 Store 接口。
// //
// Each session is stored in a separate file named {session_id}.jsonl // 每个会话存储在单独的 {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. // JSON 编码的 SessionMessage。新消息以 O(1) 时间追加。
type JSONLStore struct { type JSONLStore struct {
storageDir string storageDir string
mu sync.RWMutex mu sync.RWMutex
} }
// NewJSONLStore creates a new JSONLStore with the given storage directory. // NewJSONLStore 使用给定的存储目录创建新的 JSONLStore。
// The directory is created if it does not exist. // 如果目录不存在,则创建它。
func NewJSONLStore(storageDir string) (*JSONLStore, error) { func NewJSONLStore(storageDir string) (*JSONLStore, error) {
if err := os.MkdirAll(storageDir, 0755); err != nil { if err := os.MkdirAll(storageDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create session storage directory %q: %w", storageDir, err) 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 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 { func (s *JSONLStore) path(sessionID string) string {
return filepath.Join(s.storageDir, sessionID+".jsonl") 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 { func (s *JSONLStore) archivePath(sessionID string) string {
return filepath.Join(s.storageDir, sessionID+".jsonl.archived") return filepath.Join(s.storageDir, sessionID+".jsonl.archived")
} }
// Save appends a message to a session's JSONL file. // Save 将消息追加到会话的 JSONL 文件。
// If the file does not exist, it is created. // 如果文件不存在,则创建它。
// This is an O(1) append operation. // 这是一个 O(1) 追加操作。
func (s *JSONLStore) Save(sessionID string, msg SessionMessage) error { func (s *JSONLStore) Save(sessionID string, msg SessionMessage) error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -63,8 +64,8 @@ func (s *JSONLStore) Save(sessionID string, msg SessionMessage) error {
return nil return nil
} }
// Load retrieves all messages for a session in chronological order. // Load 按时间顺序检索会话的所有消息。
// Returns an error if the session file does not exist. // 如果会话文件不存在,则返回错误。
func (s *JSONLStore) Load(sessionID string) ([]SessionMessage, error) { func (s *JSONLStore) Load(sessionID string) ([]SessionMessage, error) {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
@ -86,7 +87,7 @@ func (s *JSONLStore) Load(sessionID string) ([]SessionMessage, error) {
return parseJSONL(data) return parseJSONL(data)
} }
// parseJSONL parses a JSONL byte slice into a slice of SessionMessage. // parseJSONL 将 JSONL 字节切片解析为 SessionMessage 切片。
func parseJSONL(data []byte) ([]SessionMessage, error) { func parseJSONL(data []byte) ([]SessionMessage, error) {
var messages []SessionMessage var messages []SessionMessage
trimmed := strings.TrimSpace(string(data)) trimmed := strings.TrimSpace(string(data))
@ -109,7 +110,7 @@ func parseJSONL(data []byte) ([]SessionMessage, error) {
return messages, nil return messages, nil
} }
// List returns all session IDs by scanning the storage directory. // List 通过扫描存储目录返回所有会话 ID。
func (s *JSONLStore) List() ([]string, error) { func (s *JSONLStore) List() ([]string, error) {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
@ -129,7 +130,7 @@ func (s *JSONLStore) List() ([]string, error) {
return sessions, nil return sessions, nil
} }
// Exists checks whether a session file exists (active or archived). // Exists 检查会话文件是否存在(活动或已归档)。
func (s *JSONLStore) Exists(sessionID string) (bool, error) { func (s *JSONLStore) Exists(sessionID string) (bool, error) {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
@ -150,7 +151,7 @@ func (s *JSONLStore) Exists(sessionID string) (bool, error) {
return false, nil return false, nil
} }
// Archive moves a session file to the archived state by renaming it. // Archive 通过重命名将会话文件移动到归档状态。
func (s *JSONLStore) Archive(sessionID string) error { func (s *JSONLStore) Archive(sessionID string) error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -164,7 +165,7 @@ func (s *JSONLStore) Archive(sessionID string) error {
return nil return nil
} }
// Delete permanently removes a session file and its archive. // Delete 永久移除会话文件及其归档。
func (s *JSONLStore) Delete(sessionID string) error { func (s *JSONLStore) Delete(sessionID string) error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -184,7 +185,7 @@ func (s *JSONLStore) Delete(sessionID string) error {
return lastErr return lastErr
} }
// StorageDir returns the storage directory path. // StorageDir 返回存储目录路径。
func (s *JSONLStore) StorageDir() string { func (s *JSONLStore) StorageDir() string {
return s.storageDir return s.storageDir
} }

View File

@ -1,3 +1,7 @@
// Package session 为 Orca 框架提供对话会话管理功能。
//
// 会话持久化对话历史记录,并为 LLM 交互提供基于上下文窗口的
// 检索功能。默认存储后端使用 JSONL 文件,支持 O(1) 追加写入。
package session package session
import ( import (
@ -8,10 +12,10 @@ import (
"github.com/orca/orca/pkg/bus" "github.com/orca/orca/pkg/bus"
) )
// Manager provides high-level session lifecycle operations. // Manager 提供高级会话生命周期操作。
// //
// It wraps a Store with caching, context window management, and // 它使用 Store 包装缓存、上下文窗口管理和
// event publishing on the message bus. // 消息总线上的事件发布。
type Manager struct { type Manager struct {
store Store store Store
bus bus.MessageBus bus bus.MessageBus
@ -19,7 +23,7 @@ type Manager struct {
mu sync.RWMutex 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 { func NewManager(store Store, mb bus.MessageBus) *Manager {
return &Manager{ return &Manager{
store: store, 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) { func (m *Manager) CreateSession(id string, metadata map[string]string) (*Session, error) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@ -62,7 +66,7 @@ func (m *Manager) CreateSession(id string, metadata map[string]string) (*Session
return session, nil 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) { func (m *Manager) GetSession(id string) (*Session, error) {
m.mu.RLock() m.mu.RLock()
session, ok := m.cache[id] session, ok := m.cache[id]
@ -106,7 +110,7 @@ func (m *Manager) GetSession(id string) (*Session, error) {
return session, nil 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) { func (m *Manager) AddMessage(sessionID string, role MessageRole, content string, metadata map[string]string) (*SessionMessage, error) {
msg := SessionMessage{ msg := SessionMessage{
Role: role, Role: role,
@ -138,8 +142,8 @@ func (m *Manager) AddMessage(sessionID string, role MessageRole, content string,
return &msg, nil return &msg, nil
} }
// GetContext returns the most recent N messages in a session. // GetContext 返回会话中最近的 N 条消息。
// If windowSize <= 0 or >= total messages, all messages are returned. // 如果 windowSize <= 0 或 >= 总消息数,则返回所有消息。
func (m *Manager) GetContext(sessionID string, windowSize int) ([]SessionMessage, error) { func (m *Manager) GetContext(sessionID string, windowSize int) ([]SessionMessage, error) {
session, err := m.GetSession(sessionID) session, err := m.GetSession(sessionID)
if err != nil { if err != nil {
@ -153,7 +157,7 @@ func (m *Manager) GetContext(sessionID string, windowSize int) ([]SessionMessage
return messages, nil return messages, nil
} }
// ArchiveSession archives a session, making it read-only. // ArchiveSession 归档会话,使其变为只读。
func (m *Manager) ArchiveSession(id string) error { func (m *Manager) ArchiveSession(id string) error {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@ -179,7 +183,7 @@ func (m *Manager) ArchiveSession(id string) error {
return nil return nil
} }
// DeleteSession permanently removes a session. // DeleteSession 永久移除会话。
func (m *Manager) DeleteSession(id string) error { func (m *Manager) DeleteSession(id string) error {
m.mu.Lock() m.mu.Lock()
delete(m.cache, id) delete(m.cache, id)
@ -187,12 +191,12 @@ func (m *Manager) DeleteSession(id string) error {
return m.store.Delete(id) return m.store.Delete(id)
} }
// ListSessions returns all known session IDs. // ListSessions 返回所有已知的会话 ID。
func (m *Manager) ListSessions() ([]string, error) { func (m *Manager) ListSessions() ([]string, error) {
return m.store.List() return m.store.List()
} }
// Store returns the underlying Store. // Store 返回底层存储。
func (m *Manager) Store() Store { func (m *Manager) Store() Store {
return m.store return m.store
} }

View File

@ -1,28 +1,29 @@
// Package session 为 Orca 框架提供对话会话管理功能。
package session package session
// Store defines the persistence interface for session message storage. // Store 定义会话消息存储的持久化接口。
// //
// Implementations must be safe for concurrent use. The default implementation // 实现必须支持并发使用。默认实现使用 JSONL 文件
// uses JSONL files (one file per session) with O(1) append writes. //(每个会话一个文件),支持 O(1) 追加写入。
type Store interface { type Store interface {
// Save appends a single message to a session's history. // Save 将单条消息追加到会话历史中。
// Creates the session file if it does not exist. // 如果会话文件不存在,则创建它。
Save(sessionID string, msg SessionMessage) error Save(sessionID string, msg SessionMessage) error
// Load retrieves all messages for a session in chronological order. // Load 按时间顺序检索会话的所有消息。
// Returns an error if the session does not exist. // 如果会话不存在,则返回错误。
Load(sessionID string) ([]SessionMessage, error) Load(sessionID string) ([]SessionMessage, error)
// List returns all known session IDs. // List 返回所有已知的会话 ID。
List() ([]string, error) List() ([]string, error)
// Exists checks whether a session exists in the store. // Exists 检查会话是否存在于存储中。
Exists(sessionID string) (bool, error) Exists(sessionID string) (bool, error)
// Archive marks a session as archived (read-only). // Archive 将会话标记为已归档(只读)。
// This is a soft delete that preserves the data. // 这是一个保留数据的软删除操作。
Archive(sessionID string) error Archive(sessionID string) error
// Delete removes a session permanently from the store. // Delete 从存储中永久移除会话。
Delete(sessionID string) error Delete(sessionID string) error
} }

View File

@ -1,3 +1,9 @@
// Package skill 提供 Skill 定义和管理系统。
//
// 技能是从 ~/.agents/skills/ 加载的可组合能力。
// 每个技能都有一个带 YAML 前置元数据的 SKILL.md 清单文件,
// 以及可选的 scripts/ 子目录中的脚本。
// 技能可以通过触发关键词被发现和调用。
package skill package skill
import ( import (
@ -10,27 +16,25 @@ import (
) )
const ( const (
// DefaultSkillsDir is the default directory for user-installed skills. // DefaultSkillsDir 是用户安装技能的默认目录。
DefaultSkillsDir = "~/.agents/skills" DefaultSkillsDir = "~/.agents/skills"
// SkillManifestFile is the name of the skill manifest file. // SkillManifestFile 是技能清单文件的名称。
SkillManifestFile = "SKILL.md" 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 // 技能从目录树加载,每个包含 SKILL.md 文件的子目录都被视为一个技能。
// 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.
type Manager struct { type Manager struct {
mu sync.RWMutex mu sync.RWMutex
skillsDir string skillsDir string
skills map[string]*Skill skills map[string]*Skill
} }
// NewManager creates a new Skill manager that scans the given directory for skills. // NewManager 创建一个新的技能管理器,扫描给定目录中的技能。
// If skillsDir is empty, DefaultSkillsDir is used. // 如果 skillsDir 为空,则使用 DefaultSkillsDir。
func NewManager(skillsDir string) *Manager { func NewManager(skillsDir string) *Manager {
if skillsDir == "" { if skillsDir == "" {
skillsDir = DefaultSkillsDir skillsDir = DefaultSkillsDir
@ -44,8 +48,8 @@ func NewManager(skillsDir string) *Manager {
} }
} }
// LoadAll scans the skills directory and loads all skills found. // LoadAll 扫描技能目录并加载所有找到的技能。
// It returns the number of skills loaded and any errors encountered. // 返回加载的技能数量和遇到的任何错误。
func (m *Manager) LoadAll() (int, error) { func (m *Manager) LoadAll() (int, error) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@ -104,7 +108,7 @@ func (m *Manager) LoadAll() (int, error) {
return loaded, nil 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) { func (m *Manager) GetSkill(name string) (*Skill, bool) {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
@ -113,7 +117,7 @@ func (m *Manager) GetSkill(name string) (*Skill, bool) {
return skill, ok return skill, ok
} }
// ListSkills returns all loaded skills sorted by name. // ListSkills 返回按名称排序的所有已加载技能。
func (m *Manager) ListSkills() []*Skill { func (m *Manager) ListSkills() []*Skill {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
@ -129,8 +133,8 @@ func (m *Manager) ListSkills() []*Skill {
return result return result
} }
// FindSkill finds skills whose triggers match the given query string. // FindSkill 查找触发器与给定查询字符串匹配的技能。
// Returns all matching skills sorted by relevance (more trigger matches first). // 返回所有匹配的技能,按相关性排序(触发器匹配多的优先)。
func (m *Manager) FindSkill(query string) []*Skill { func (m *Manager) FindSkill(query string) []*Skill {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
@ -150,17 +154,17 @@ func (m *Manager) FindSkill(query string) []*Skill {
return matches return matches
} }
// SkillsDir returns the directory being scanned for skills. // SkillsDir 返回正在扫描技能的目录。
func (m *Manager) SkillsDir() string { func (m *Manager) SkillsDir() string {
return m.skillsDir return m.skillsDir
} }
// Reload refreshes all skills from disk. // Reload 从磁盘刷新所有技能。
func (m *Manager) Reload() (int, error) { func (m *Manager) Reload() (int, error) {
return m.LoadAll() return m.LoadAll()
} }
// countMatches counts how many of the skill's triggers match the query. // countMatches 计算技能的触发器中有多少个与查询匹配。
func countMatches(skill *Skill, query string) int { func countMatches(skill *Skill, query string) int {
count := 0 count := 0
queryLower := strings.ToLower(query) queryLower := strings.ToLower(query)

View File

@ -1,3 +1,4 @@
// Package skill 提供 Skill 定义和管理系统。
package skill package skill
import ( import (
@ -8,14 +9,14 @@ import (
"strings" "strings"
) )
// FrontmatterDelimiters for YAML frontmatter in SKILL.md files. // frontmatterDelim 是 SKILL.md 文件中 YAML 前置元数据的分隔符。
const ( const (
frontmatterDelim = "---" frontmatterDelim = "---"
) )
// ParseSkillFile parses a SKILL.md file and returns a populated Skill struct. // ParseSkillFile 解析 SKILL.md 文件并返回填充好的 Skill 结构体。
// //
// The expected format is: // 预期格式为:
// //
// --- // ---
// name: my-skill // name: my-skill
@ -35,8 +36,8 @@ func ParseSkillFile(path string) (*Skill, error) {
return ParseSkillData(path, data) return ParseSkillData(path, data)
} }
// ParseSkillData parses SKILL.md content from raw bytes. // ParseSkillData 从原始字节解析 SKILL.md 内容。
// The path parameter is used to locate the scripts/ directory. // path 参数用于定位 scripts/ 目录。
func ParseSkillData(path string, data []byte) (*Skill, error) { func ParseSkillData(path string, data []byte) (*Skill, error) {
content := string(data) content := string(data)
@ -74,8 +75,8 @@ func ParseSkillData(path string, data []byte) (*Skill, error) {
return skill, nil return skill, nil
} }
// parseFrontmatter extracts YAML frontmatter delimited by "---" lines // parseFrontmatter 提取由 "---" 行分隔的 YAML 前置元数据
// and populates the Skill struct fields. // 并填充 Skill 结构体字段。
func parseFrontmatter(content string, skill *Skill) (string, error) { func parseFrontmatter(content string, skill *Skill) (string, error) {
content = strings.TrimSpace(content) content = strings.TrimSpace(content)
@ -108,8 +109,8 @@ func parseFrontmatter(content string, skill *Skill) (string, error) {
return body, nil return body, nil
} }
// parseSimpleYAML parses a simplified YAML format for skill frontmatter. // parseSimpleYAML 解析技能前置元数据的简化 YAML 格式。
// Supports: string values, quoted strings, and array values. // 支持:字符串值、带引号的字符串和数组值。
func parseSimpleYAML(yaml string, skill *Skill) error { func parseSimpleYAML(yaml string, skill *Skill) error {
lines := strings.Split(yaml, "\n") lines := strings.Split(yaml, "\n")
for _, line := range lines { for _, line := range lines {
@ -144,7 +145,7 @@ func parseSimpleYAML(yaml string, skill *Skill) error {
return nil 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) { func parseYAMLArray(value string) ([]string, error) {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
@ -175,7 +176,7 @@ func parseYAMLArray(value string) ([]string, error) {
return []string{}, nil return []string{}, nil
} }
// splitCommas splits a comma-separated string respecting quoted sections. // splitCommas 分割逗号分隔的字符串,尊重引号部分。
func splitCommas(s string) []string { func splitCommas(s string) []string {
var parts []string var parts []string
var current strings.Builder var current strings.Builder
@ -206,7 +207,7 @@ func splitCommas(s string) []string {
return parts return parts
} }
// trimQuotes removes surrounding quotes from a string value. // trimQuotes 移除字符串值周围的引号。
func trimQuotes(s string) string { func trimQuotes(s string) string {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
if len(s) >= 2 { if len(s) >= 2 {
@ -217,7 +218,7 @@ func trimQuotes(s string) string {
return s return s
} }
// discoverScripts lists all executable/readable files in a scripts directory. // discoverScripts 列出 scripts 目录中所有可执行/可读的文件。
func discoverScripts(scriptsDir string) ([]string, error) { func discoverScripts(scriptsDir string) ([]string, error) {
entries, err := os.ReadDir(scriptsDir) entries, err := os.ReadDir(scriptsDir)
if err != nil { if err != nil {
@ -236,7 +237,7 @@ func discoverScripts(scriptsDir string) ([]string, error) {
return scripts, nil return scripts, nil
} }
// LoadSkillFromDir loads a skill from a directory containing a SKILL.md file. // LoadSkillFromDir 从包含 SKILL.md 文件的目录加载技能。
func LoadSkillFromDir(dir string) (*Skill, error) { func LoadSkillFromDir(dir string) (*Skill, error) {
skillPath := filepath.Join(dir, "SKILL.md") skillPath := filepath.Join(dir, "SKILL.md")
if _, err := os.Stat(skillPath); os.IsNotExist(err) { if _, err := os.Stat(skillPath); os.IsNotExist(err) {

View File

@ -1,9 +1,9 @@
// Package skill provides the Skill definition and management system. // Package skill 提供 Skill 定义和管理系统。
// //
// Skills are composable capabilities loaded from ~/.agents/skills/. // 技能是从 ~/.agents/skills/ 加载的可组合能力。
// Each skill has a SKILL.md manifest with YAML frontmatter and optional // 每个技能都有一个带 YAML 前置元数据的 SKILL.md 清单文件,
// scripts in a scripts/ subdirectory. Skills can be discovered and // 以及可选的 scripts/ 子目录中的脚本。
// invoked by trigger keywords. // 技能可以通过触发关键词被发现和调用。
package skill package skill
import ( import (
@ -11,30 +11,29 @@ import (
"strings" "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 // 每个技能由 SKILL.md 文件定义,其中包含 YAML 前置元数据(名称、描述、触发器)
// metadata (name, description, triggers) and optional executable scripts // 以及可选的 scripts/ 子目录中的可执行脚本。
// in a scripts/ subdirectory.
type Skill struct { type Skill struct {
// Name is the unique identifier for this skill (e.g., "dev-browser"). // Name 是此技能的唯一标识符(例如 "dev-browser")。
Name string `yaml:"name"` Name string `yaml:"name"`
// Description is a human-readable explanation of what this skill does. // Description 是对此技能功能的可读说明。
Description string `yaml:"description"` Description string `yaml:"description"`
// Triggers are keywords that activate this skill from natural language. // Triggers 是从自然语言中激活此技能的关键词。
Triggers []string `yaml:"triggers"` Triggers []string `yaml:"triggers"`
// Scripts is the list of script file names in the scripts/ directory. // Scripts 是 scripts/ 目录中的脚本文件名称列表。
Scripts []string `yaml:"-"` Scripts []string `yaml:"-"`
// ScriptsDir is the absolute path to the scripts/ directory. // ScriptsDir 是 scripts/ 目录的绝对路径。
ScriptsDir string `yaml:"-"` ScriptsDir string `yaml:"-"`
// Body is the markdown content after the YAML frontmatter. // Body 是 YAML 前置元数据之后的 Markdown 内容。
Body string `yaml:"-"` Body string `yaml:"-"`
// Path is the absolute path to the SKILL.md file. // Path 是 SKILL.md 文件的绝对路径。
Path string `yaml:"-"` Path string `yaml:"-"`
} }
// MatchTrigger checks if the given query matches any of the skill's triggers. // MatchTrigger 检查给定查询是否与技能的任何触发器匹配。
// Matching is case-insensitive and supports partial matches. // 匹配不区分大小写,支持部分匹配。
func (s *Skill) MatchTrigger(query string) bool { func (s *Skill) MatchTrigger(query string) bool {
query = strings.ToLower(query) query = strings.ToLower(query)
for _, trigger := range s.Triggers { for _, trigger := range s.Triggers {
@ -45,12 +44,12 @@ func (s *Skill) MatchTrigger(query string) bool {
return false return false
} }
// String returns a human-readable representation of the skill. // String 返回技能的可读表示形式。
func (s *Skill) String() string { func (s *Skill) String() string {
return fmt.Sprintf("Skill{Name: %q, Triggers: %v, Scripts: %d}", s.Name, s.Triggers, len(s.Scripts)) 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 { func (s *Skill) HasScripts() bool {
return len(s.Scripts) > 0 return len(s.Scripts) > 0
} }

79
pkg/tool/agent_call.go Normal file
View File

@ -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
}

View File

@ -45,9 +45,9 @@ func (t *execTool) Parameters() map[string]ParameterSchema {
}, },
"timeout": { "timeout": {
Type: "number", Type: "number",
Description: "Timeout in seconds for the command execution (default: 30)", Description: "Timeout in seconds for the command execution (default: 120)",
Required: false, Required: false,
Default: float64(30), Default: float64(120),
}, },
"workdir": { "workdir": {
Type: "string", Type: "string",

View File

@ -1,3 +1,8 @@
// Package tool 定义 Tool 接口和工具管理系统。
//
// 工具是可以由智能体或 LLM 调用的原子能力。
// 每个工具都有名称、描述、参数模式(用于 LLM 函数调用)
// 和执行实际工作的 Execute 方法。
package tool package tool
import ( import (
@ -7,25 +12,23 @@ import (
"sync" "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 { type Manager struct {
mu sync.RWMutex mu sync.RWMutex
tools map[string]Tool tools map[string]Tool
} }
// NewManager creates a new empty tool manager. // NewManager 创建新的空工具管理器。
func NewManager() *Manager { func NewManager() *Manager {
return &Manager{ return &Manager{
tools: make(map[string]Tool), tools: make(map[string]Tool),
} }
} }
// Register adds a tool to the manager. Returns an error if a tool with the // Register 将工具添加到管理器。如果已注册了同名工具,则返回错误。
// same name is already registered.
func (m *Manager) Register(tool Tool) error { func (m *Manager) Register(tool Tool) error {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@ -39,7 +42,7 @@ func (m *Manager) Register(tool Tool) error {
return nil return nil
} }
// Unregister removes a tool from the manager by name. // Unregister 按名称从管理器中移除工具。
func (m *Manager) Unregister(name string) error { func (m *Manager) Unregister(name string) error {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@ -52,7 +55,7 @@ func (m *Manager) Unregister(name string) error {
return nil return nil
} }
// Get retrieves a tool by name. Returns false if not found. // Get 按名称检索工具。如果未找到则返回 false。
func (m *Manager) Get(name string) (Tool, bool) { func (m *Manager) Get(name string) (Tool, bool) {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
@ -61,7 +64,7 @@ func (m *Manager) Get(name string) (Tool, bool) {
return t, ok return t, ok
} }
// List returns all registered tools sorted by name. // List 返回按名称排序的所有已注册工具。
func (m *Manager) List() []Tool { func (m *Manager) List() []Tool {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
@ -77,8 +80,8 @@ func (m *Manager) List() []Tool {
return result return result
} }
// Execute looks up a tool by name and invokes it with the given arguments. // Execute 按名称查找工具并使用给定参数调用它。
// Returns an error if the tool is not found. // 如果未找到工具则返回错误。
func (m *Manager) Execute(name string, ctx context.Context, args map[string]interface{}) (*Result, error) { func (m *Manager) Execute(name string, ctx context.Context, args map[string]interface{}) (*Result, error) {
tool, ok := m.Get(name) tool, ok := m.Get(name)
if !ok { if !ok {
@ -87,14 +90,14 @@ func (m *Manager) Execute(name string, ctx context.Context, args map[string]inte
return tool.Execute(ctx, args) return tool.Execute(ctx, args)
} }
// Count returns the number of registered tools. // Count 返回已注册工具的数量。
func (m *Manager) Count() int { func (m *Manager) Count() int {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
return len(m.tools) return len(m.tools)
} }
// Names returns the names of all registered tools sorted alphabetically. // Names 返回按字母顺序排序的所有已注册工具名称。
func (m *Manager) Names() []string { func (m *Manager) Names() []string {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()

View File

@ -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`