picoclaw/cmd/piconomous/internal/agent/helpers.go
Claude b7b671f1c1
rename picoclaw -> piconomous + add fully autonomous mode
## 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
2026-03-21 15:17:32 +00:00

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