## Rename - Module path: github.com/sipeed/picoclaw -> github.com/sipeed/piconomous - All binary names: picoclaw* -> piconomous* - Config/data paths: ~/.picoclaw -> ~/.piconomous - Env vars: PICOCLAW_* -> PICONOMOUS_* - Directory renames: cmd/picoclaw -> cmd/piconomous, cmd/picoclaw-launcher-tui -> cmd/piconomous-launcher-tui - Asset renames, desktop file, docker services ## Fully Autonomous Mode (new feature) Adds `autonomous` config section with: - `enabled`: activates goal-driven autonomous operation - `interval_minutes`: heartbeat frequency (default 5 min in autonomous mode) - `allow_self_schedule`: agent can create cron jobs for itself without command_confirm - `max_goals`: cap on concurrent active goals (default 20) ### New: Goal Management Tool (pkg/tools/goal.go) - `goal` tool with actions: set/list/complete/update/drop - Goals persist in GOALS.md (human-readable) + goals.json (machine-readable) - Priority levels P1-P5, status lifecycle: active -> completed/dropped ### Enhanced Heartbeat Service - SetAutonomous(bool) activates goal-driven prompt mode - In autonomous mode: reads GOALS.md each cycle, builds a rich prompt that instructs the agent to run the full software dev lifecycle: write code, run tests, fix failures, commit progress, schedule next steps - Creates default GOALS.md template on first run - Autonomous interval overrides heartbeat interval (5 min default vs 30 min) ### Cron Tool - In autonomous mode, agent can self-schedule on internal channels without requiring explicit command_confirm=true (GHSA-pv8c-p6jf-3fpp exception) ### Goal Tool Registration - Registered in AgentLoop.registerSharedTools when autonomous.enabled=true or when tools.goal is explicitly enabled in config https://claude.ai/code/session_0118WWK5KLRM7ZBUoC8EMgKS
163 lines
3.7 KiB
Go
163 lines
3.7 KiB
Go
package agent
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/ergochat/readline"
|
|
|
|
"github.com/sipeed/piconomous/cmd/piconomous/internal"
|
|
"github.com/sipeed/piconomous/pkg/agent"
|
|
"github.com/sipeed/piconomous/pkg/bus"
|
|
"github.com/sipeed/piconomous/pkg/logger"
|
|
"github.com/sipeed/piconomous/pkg/providers"
|
|
)
|
|
|
|
func agentCmd(message, sessionKey, model string, debug bool) error {
|
|
if sessionKey == "" {
|
|
sessionKey = "cli:default"
|
|
}
|
|
|
|
if debug {
|
|
logger.SetLevel(logger.DEBUG)
|
|
fmt.Println("🔍 Debug mode enabled")
|
|
}
|
|
|
|
cfg, err := internal.LoadConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("error loading config: %w", err)
|
|
}
|
|
|
|
if model != "" {
|
|
cfg.Agents.Defaults.ModelName = model
|
|
}
|
|
|
|
provider, modelID, err := providers.CreateProvider(cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating provider: %w", err)
|
|
}
|
|
|
|
// Use the resolved model ID from provider creation
|
|
if modelID != "" {
|
|
cfg.Agents.Defaults.ModelName = modelID
|
|
}
|
|
|
|
msgBus := bus.NewMessageBus()
|
|
defer msgBus.Close()
|
|
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
|
defer agentLoop.Close()
|
|
|
|
// 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 {
|
|
return fmt.Errorf("error processing message: %w", err)
|
|
}
|
|
fmt.Printf("\n%s %s\n", internal.Logo, response)
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", internal.Logo)
|
|
interactiveMode(agentLoop, sessionKey)
|
|
|
|
return nil
|
|
}
|
|
|
|
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|
prompt := fmt.Sprintf("%s You: ", internal.Logo)
|
|
|
|
rl, err := readline.NewEx(&readline.Config{
|
|
Prompt: prompt,
|
|
HistoryFile: filepath.Join(os.TempDir(), ".piconomous_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", internal.Logo, response)
|
|
}
|
|
}
|
|
|
|
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
for {
|
|
fmt.Print(fmt.Sprintf("%s You: ", internal.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", internal.Logo, response)
|
|
}
|
|
}
|