Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions pkg/agent/multi/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors

// Package multi provides the foundation for multi-agent collaboration.
// It defines the Agent interface, shared context, and agent registry
// that enable multiple specialized agents to work together within
// a single PicoClaw session.
//
// This package is designed to be non-invasive: it introduces new
// abstractions without modifying the existing AgentLoop or SubagentManager.
// The existing subagent system can be gradually migrated to use these
// interfaces.
package multi

import (
"context"

"github.com/sipeed/picoclaw/pkg/tools"
)

// Agent defines the interface that all agents must implement.
// Each agent has a unique name, a role description, a system prompt,
// and a set of capabilities that determine which tasks it can handle.
type Agent interface {
// Name returns the unique identifier for this agent.
Name() string

// Role returns a human-readable description of what this agent does.
Role() string

// SystemPrompt returns the system prompt used when this agent
// interacts with the LLM.
SystemPrompt() string

// Capabilities returns the list of capability tags this agent supports.
// These are used by the registry to match agents to tasks.
// Example: ["code", "search", "file_operations"]
Capabilities() []string

// Tools returns the tool registry available to this agent.
// Each agent can have a different set of tools.
Tools() *tools.ToolRegistry

// Execute runs the agent on the given task within the provided context.
// The shared context allows reading/writing data visible to other agents.
// Returns the agent's response content and any error.
Execute(ctx context.Context, task string, shared *SharedContext) (string, error)
}

// AgentConfig holds configuration for creating a BaseAgent.
type AgentConfig struct {
// Name is the unique identifier for the agent.
Name string

// Role describes what this agent specializes in.
Role string

// SystemPrompt is the prompt sent to the LLM.
SystemPrompt string

// Capabilities lists the capability tags.
Capabilities []string
}

// BaseAgent provides a minimal Agent implementation that can be embedded
// in concrete agent types. It handles the common fields (name, role, prompt,
// capabilities) and leaves Execute to be implemented by the concrete type.
type BaseAgent struct {
config AgentConfig
tools *tools.ToolRegistry
}

// NewBaseAgent creates a new BaseAgent with the given configuration.
func NewBaseAgent(cfg AgentConfig, registry *tools.ToolRegistry) *BaseAgent {
if registry == nil {
registry = tools.NewToolRegistry()
}
return &BaseAgent{
config: cfg,
tools: registry,
}
}

func (a *BaseAgent) Name() string { return a.config.Name }
func (a *BaseAgent) Role() string { return a.config.Role }
func (a *BaseAgent) SystemPrompt() string { return a.config.SystemPrompt }
func (a *BaseAgent) Capabilities() []string { return a.config.Capabilities }
func (a *BaseAgent) Tools() *tools.ToolRegistry { return a.tools }

// HandoffRequest represents a request to delegate a task from one agent
// to another. It carries the task description and optional metadata
// for routing.
type HandoffRequest struct {
// FromAgent is the name of the agent delegating the task.
FromAgent string

// ToAgent is the name of the target agent. If empty, the registry
// will select the best agent based on RequiredCapability.
ToAgent string

// RequiredCapability is used for capability-based routing when
// ToAgent is not specified.
RequiredCapability string

// Task is the description of what needs to be done.
Task string

// Context carries additional key-value data for the target agent.
Context map[string]interface{}
}

// HandoffResult contains the outcome of a hand-off operation.
type HandoffResult struct {
// AgentName is the name of the agent that handled the task.
AgentName string

// Content is the response produced by the agent.
Content string

// Err is set if the hand-off or execution failed.
Err error
}
151 changes: 151 additions & 0 deletions pkg/agent/multi/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors

package multi

import (
"sync"
)

// SharedContext implements a thread-safe blackboard pattern where multiple
// agents can read from and write to a common session context.
//
// It provides:
// - Key-value storage for arbitrary data sharing between agents
// - An append-only event log for agent activity tracking
// - Thread-safe access via read-write mutex
//
// This is intentionally simple and in-memory. Future iterations may add
// persistence, TTL, or namespace isolation.
type SharedContext struct {
mu sync.RWMutex
data map[string]interface{}
events []Event
}

// Event records an action taken by an agent within the shared context.
// Events are append-only and provide an audit trail of agent activity.
type Event struct {
// Agent is the name of the agent that produced this event.
Agent string

// Type categorizes the event (e.g., "handoff", "result", "error").
Type string

// Content is the event payload.
Content string
}

// NewSharedContext creates a new empty SharedContext.
func NewSharedContext() *SharedContext {
return &SharedContext{
data: make(map[string]interface{}),
events: make([]Event, 0),
}
}

// Set stores a value in the shared context under the given key.
// Overwrites any existing value for the same key.
func (sc *SharedContext) Set(key string, value interface{}) {
sc.mu.Lock()
defer sc.mu.Unlock()
sc.data[key] = value
}

// Get retrieves a value from the shared context.
// Returns the value and true if found, nil and false otherwise.
func (sc *SharedContext) Get(key string) (interface{}, bool) {
sc.mu.RLock()
defer sc.mu.RUnlock()
v, ok := sc.data[key]
return v, ok
}

// GetString retrieves a string value from the shared context.
// Returns empty string if the key doesn't exist or isn't a string.
func (sc *SharedContext) GetString(key string) string {
v, ok := sc.Get(key)
if !ok {
return ""
}
s, _ := v.(string)
return s
}

// Delete removes a key from the shared context.
func (sc *SharedContext) Delete(key string) {
sc.mu.Lock()
defer sc.mu.Unlock()
delete(sc.data, key)
}

// Keys returns all keys currently stored in the shared context.
func (sc *SharedContext) Keys() []string {
sc.mu.RLock()
defer sc.mu.RUnlock()
keys := make([]string, 0, len(sc.data))
for k := range sc.data {
keys = append(keys, k)
}
return keys
}

// AddEvent appends an event to the shared context's event log.
func (sc *SharedContext) AddEvent(agent, eventType, content string) {
sc.mu.Lock()
defer sc.mu.Unlock()
sc.events = append(sc.events, Event{
Agent: agent,
Type: eventType,
Content: content,
})
}

// Events returns a copy of all events in the shared context.
func (sc *SharedContext) Events() []Event {
sc.mu.RLock()
defer sc.mu.RUnlock()
cp := make([]Event, len(sc.events))
copy(cp, sc.events)
return cp
}

// EventsByAgent returns all events produced by the given agent.
func (sc *SharedContext) EventsByAgent(agent string) []Event {
sc.mu.RLock()
defer sc.mu.RUnlock()
var filtered []Event
for _, e := range sc.events {
if e.Agent == agent {
filtered = append(filtered, e)
}
}
return filtered
}

// EventsByType returns all events of the given type.
func (sc *SharedContext) EventsByType(eventType string) []Event {
sc.mu.RLock()
defer sc.mu.RUnlock()
var filtered []Event
for _, e := range sc.events {
if e.Type == eventType {
filtered = append(filtered, e)
}
}
return filtered
}

// Snapshot returns a shallow copy of the entire data map.
// Useful for debugging or serialization.
func (sc *SharedContext) Snapshot() map[string]interface{} {
sc.mu.RLock()
defer sc.mu.RUnlock()
snap := make(map[string]interface{}, len(sc.data))
for k, v := range sc.data {
snap[k] = v
}
return snap
}
Loading