mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
24e2ed79c0
PublishInbound/PublishOutbound held RLock during blocking channel sends, deadlocking against Close() which needs a write lock when the buffer is full. ConsumeInbound/SubscribeOutbound used bare receives instead of comma-ok, causing zero-value processing or busy loops after close. Replace sync.RWMutex+bool with atomic.Bool+done channel so Publish methods use a lock-free 3-way select (send / done / ctx.Done). Add context.Context parameter to both Publish methods so callers can cancel or timeout blocked sends. Close() now only sets the atomic flag and closes the done channel—never closes the data channels—eliminating send-on-closed-channel panics. - Remove dead code: RegisterHandler, GetHandler, handlers map, MessageHandler type (zero callers across the whole repo) - Add ErrBusClosed sentinel error - Update all 10 caller sites to pass context - Add msgBus.Close() to gateway and agent shutdown flows - Add pkg/bus/bus_test.go with 11 test cases covering basic round-trip, context cancellation, closed-bus behavior, concurrent publish+close, full-buffer timeout, and idempotent Close
183 lines
3.9 KiB
Go
183 lines
3.9 KiB
Go
// PicoClaw - Ultra-lightweight personal AI agent
|
|
// License: MIT
|
|
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/chzyer/readline"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/agent"
|
|
"github.com/sipeed/picoclaw/pkg/bus"
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
|
"github.com/sipeed/picoclaw/pkg/providers"
|
|
)
|
|
|
|
func agentCmd() {
|
|
message := ""
|
|
sessionKey := "cli:default"
|
|
modelOverride := ""
|
|
|
|
args := os.Args[2:]
|
|
for i := 0; i < len(args); i++ {
|
|
switch args[i] {
|
|
case "--debug", "-d":
|
|
logger.SetLevel(logger.DEBUG)
|
|
fmt.Println("🔍 Debug mode enabled")
|
|
case "-m", "--message":
|
|
if i+1 < len(args) {
|
|
message = args[i+1]
|
|
i++
|
|
}
|
|
case "-s", "--session":
|
|
if i+1 < len(args) {
|
|
sessionKey = args[i+1]
|
|
i++
|
|
}
|
|
case "--model", "-model":
|
|
if i+1 < len(args) {
|
|
modelOverride = args[i+1]
|
|
i++
|
|
}
|
|
}
|
|
}
|
|
|
|
cfg, err := loadConfig()
|
|
if err != nil {
|
|
fmt.Printf("Error loading config: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if modelOverride != "" {
|
|
cfg.Agents.Defaults.Model = modelOverride
|
|
}
|
|
|
|
provider, modelID, err := providers.CreateProvider(cfg)
|
|
if err != nil {
|
|
fmt.Printf("Error creating provider: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
// Use the resolved model ID from provider creation
|
|
if modelID != "" {
|
|
cfg.Agents.Defaults.Model = modelID
|
|
}
|
|
|
|
msgBus := bus.NewMessageBus()
|
|
defer msgBus.Close()
|
|
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
|
|
|
// Print agent startup info (only for interactive mode)
|
|
startupInfo := agentLoop.GetStartupInfo()
|
|
logger.InfoCF("agent", "Agent initialized",
|
|
map[string]any{
|
|
"tools_count": startupInfo["tools"].(map[string]any)["count"],
|
|
"skills_total": startupInfo["skills"].(map[string]any)["total"],
|
|
"skills_available": startupInfo["skills"].(map[string]any)["available"],
|
|
})
|
|
|
|
if message != "" {
|
|
ctx := context.Background()
|
|
response, err := agentLoop.ProcessDirect(ctx, message, sessionKey)
|
|
if err != nil {
|
|
fmt.Printf("Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("\n%s %s\n", logo, response)
|
|
} else {
|
|
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", logo)
|
|
interactiveMode(agentLoop, sessionKey)
|
|
}
|
|
}
|
|
|
|
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|
prompt := fmt.Sprintf("%s You: ", logo)
|
|
|
|
rl, err := readline.NewEx(&readline.Config{
|
|
Prompt: prompt,
|
|
HistoryFile: filepath.Join(os.TempDir(), ".picoclaw_history"),
|
|
HistoryLimit: 100,
|
|
InterruptPrompt: "^C",
|
|
EOFPrompt: "exit",
|
|
})
|
|
if err != nil {
|
|
fmt.Printf("Error initializing readline: %v\n", err)
|
|
fmt.Println("Falling back to simple input mode...")
|
|
simpleInteractiveMode(agentLoop, sessionKey)
|
|
return
|
|
}
|
|
defer rl.Close()
|
|
|
|
for {
|
|
line, err := rl.Readline()
|
|
if err != nil {
|
|
if err == readline.ErrInterrupt || err == io.EOF {
|
|
fmt.Println("\nGoodbye!")
|
|
return
|
|
}
|
|
fmt.Printf("Error reading input: %v\n", err)
|
|
continue
|
|
}
|
|
|
|
input := strings.TrimSpace(line)
|
|
if input == "" {
|
|
continue
|
|
}
|
|
|
|
if input == "exit" || input == "quit" {
|
|
fmt.Println("Goodbye!")
|
|
return
|
|
}
|
|
|
|
ctx := context.Background()
|
|
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
|
if err != nil {
|
|
fmt.Printf("Error: %v\n", err)
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("\n%s %s\n\n", logo, response)
|
|
}
|
|
}
|
|
|
|
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
for {
|
|
fmt.Printf("%s You: ", logo)
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
fmt.Println("\nGoodbye!")
|
|
return
|
|
}
|
|
fmt.Printf("Error reading input: %v\n", err)
|
|
continue
|
|
}
|
|
|
|
input := strings.TrimSpace(line)
|
|
if input == "" {
|
|
continue
|
|
}
|
|
|
|
if input == "exit" || input == "quit" {
|
|
fmt.Println("Goodbye!")
|
|
return
|
|
}
|
|
|
|
ctx := context.Background()
|
|
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
|
if err != nil {
|
|
fmt.Printf("Error: %v\n", err)
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("\n%s %s\n\n", logo, response)
|
|
}
|
|
}
|