feat: add multi-agent routing with declarative bindings
Implement per-agent workspace/model/session isolation with 7-level
priority routing cascade (peer > parent_peer > guild > team > account >
channel > default). Backward compatible - empty agents.list creates
implicit "main" agent from defaults.
Core components:
- routing/agent_id.go: ID normalization with pre-compiled regex
- routing/session_key.go: 4 DM scope modes with identity links
- routing/route.go: RouteResolver with priority-based binding matcher
- agent/instance.go: Per-agent state (workspace, sessions, tools, model)
- agent/registry.go: Agent lifecycle, route resolution, subagent ACL
Integration:
- config.go: AgentModelConfig (flexible JSON), bindings, session config
- loop.go: Complete rewrite for multi-agent dispatch
- Channel adapters: peer_kind/peer_id metadata (telegram, discord, slack)
- spawn.go: Subagent allowlist enforcement per agent
Validated end-to-end with Discord channel-based bindings, default
fallback routing, and per-agent session persistence.
2026-02-13 15:12:33 +00:00
|
|
|
package agent
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/providers"
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/routing"
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/session"
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/tools"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// AgentInstance represents a fully configured agent with its own workspace,
|
|
|
|
|
// session manager, context builder, and tool registry.
|
|
|
|
|
type AgentInstance struct {
|
|
|
|
|
ID string
|
|
|
|
|
Name string
|
|
|
|
|
Model string
|
|
|
|
|
Fallbacks []string
|
|
|
|
|
Workspace string
|
|
|
|
|
MaxIterations int
|
2026-02-19 18:16:37 +00:00
|
|
|
MaxTokens int
|
|
|
|
|
Temperature float64
|
feat: add multi-agent routing with declarative bindings
Implement per-agent workspace/model/session isolation with 7-level
priority routing cascade (peer > parent_peer > guild > team > account >
channel > default). Backward compatible - empty agents.list creates
implicit "main" agent from defaults.
Core components:
- routing/agent_id.go: ID normalization with pre-compiled regex
- routing/session_key.go: 4 DM scope modes with identity links
- routing/route.go: RouteResolver with priority-based binding matcher
- agent/instance.go: Per-agent state (workspace, sessions, tools, model)
- agent/registry.go: Agent lifecycle, route resolution, subagent ACL
Integration:
- config.go: AgentModelConfig (flexible JSON), bindings, session config
- loop.go: Complete rewrite for multi-agent dispatch
- Channel adapters: peer_kind/peer_id metadata (telegram, discord, slack)
- spawn.go: Subagent allowlist enforcement per agent
Validated end-to-end with Discord channel-based bindings, default
fallback routing, and per-agent session persistence.
2026-02-13 15:12:33 +00:00
|
|
|
ContextWindow int
|
|
|
|
|
Provider providers.LLMProvider
|
|
|
|
|
Sessions *session.SessionManager
|
|
|
|
|
ContextBuilder *ContextBuilder
|
|
|
|
|
Tools *tools.ToolRegistry
|
|
|
|
|
Subagents *config.SubagentsConfig
|
|
|
|
|
SkillsFilter []string
|
|
|
|
|
Candidates []providers.FallbackCandidate
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewAgentInstance creates an agent instance from config.
|
|
|
|
|
func NewAgentInstance(
|
|
|
|
|
agentCfg *config.AgentConfig,
|
|
|
|
|
defaults *config.AgentDefaults,
|
2026-02-18 14:39:14 +00:00
|
|
|
cfg *config.Config,
|
feat: add multi-agent routing with declarative bindings
Implement per-agent workspace/model/session isolation with 7-level
priority routing cascade (peer > parent_peer > guild > team > account >
channel > default). Backward compatible - empty agents.list creates
implicit "main" agent from defaults.
Core components:
- routing/agent_id.go: ID normalization with pre-compiled regex
- routing/session_key.go: 4 DM scope modes with identity links
- routing/route.go: RouteResolver with priority-based binding matcher
- agent/instance.go: Per-agent state (workspace, sessions, tools, model)
- agent/registry.go: Agent lifecycle, route resolution, subagent ACL
Integration:
- config.go: AgentModelConfig (flexible JSON), bindings, session config
- loop.go: Complete rewrite for multi-agent dispatch
- Channel adapters: peer_kind/peer_id metadata (telegram, discord, slack)
- spawn.go: Subagent allowlist enforcement per agent
Validated end-to-end with Discord channel-based bindings, default
fallback routing, and per-agent session persistence.
2026-02-13 15:12:33 +00:00
|
|
|
provider providers.LLMProvider,
|
|
|
|
|
) *AgentInstance {
|
|
|
|
|
workspace := resolveAgentWorkspace(agentCfg, defaults)
|
2026-02-18 19:48:23 +00:00
|
|
|
os.MkdirAll(workspace, 0o755)
|
feat: add multi-agent routing with declarative bindings
Implement per-agent workspace/model/session isolation with 7-level
priority routing cascade (peer > parent_peer > guild > team > account >
channel > default). Backward compatible - empty agents.list creates
implicit "main" agent from defaults.
Core components:
- routing/agent_id.go: ID normalization with pre-compiled regex
- routing/session_key.go: 4 DM scope modes with identity links
- routing/route.go: RouteResolver with priority-based binding matcher
- agent/instance.go: Per-agent state (workspace, sessions, tools, model)
- agent/registry.go: Agent lifecycle, route resolution, subagent ACL
Integration:
- config.go: AgentModelConfig (flexible JSON), bindings, session config
- loop.go: Complete rewrite for multi-agent dispatch
- Channel adapters: peer_kind/peer_id metadata (telegram, discord, slack)
- spawn.go: Subagent allowlist enforcement per agent
Validated end-to-end with Discord channel-based bindings, default
fallback routing, and per-agent session persistence.
2026-02-13 15:12:33 +00:00
|
|
|
|
|
|
|
|
model := resolveAgentModel(agentCfg, defaults)
|
|
|
|
|
fallbacks := resolveAgentFallbacks(agentCfg, defaults)
|
|
|
|
|
|
|
|
|
|
restrict := defaults.RestrictToWorkspace
|
|
|
|
|
toolsRegistry := tools.NewToolRegistry()
|
|
|
|
|
toolsRegistry.Register(tools.NewReadFileTool(workspace, restrict))
|
|
|
|
|
toolsRegistry.Register(tools.NewWriteFileTool(workspace, restrict))
|
|
|
|
|
toolsRegistry.Register(tools.NewListDirTool(workspace, restrict))
|
2026-02-18 14:39:14 +00:00
|
|
|
toolsRegistry.Register(tools.NewExecToolWithConfig(workspace, restrict, cfg))
|
feat: add multi-agent routing with declarative bindings
Implement per-agent workspace/model/session isolation with 7-level
priority routing cascade (peer > parent_peer > guild > team > account >
channel > default). Backward compatible - empty agents.list creates
implicit "main" agent from defaults.
Core components:
- routing/agent_id.go: ID normalization with pre-compiled regex
- routing/session_key.go: 4 DM scope modes with identity links
- routing/route.go: RouteResolver with priority-based binding matcher
- agent/instance.go: Per-agent state (workspace, sessions, tools, model)
- agent/registry.go: Agent lifecycle, route resolution, subagent ACL
Integration:
- config.go: AgentModelConfig (flexible JSON), bindings, session config
- loop.go: Complete rewrite for multi-agent dispatch
- Channel adapters: peer_kind/peer_id metadata (telegram, discord, slack)
- spawn.go: Subagent allowlist enforcement per agent
Validated end-to-end with Discord channel-based bindings, default
fallback routing, and per-agent session persistence.
2026-02-13 15:12:33 +00:00
|
|
|
toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict))
|
|
|
|
|
toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict))
|
|
|
|
|
|
|
|
|
|
sessionsDir := filepath.Join(workspace, "sessions")
|
|
|
|
|
sessionsManager := session.NewSessionManager(sessionsDir)
|
|
|
|
|
|
|
|
|
|
contextBuilder := NewContextBuilder(workspace)
|
|
|
|
|
|
|
|
|
|
agentID := routing.DefaultAgentID
|
|
|
|
|
agentName := ""
|
|
|
|
|
var subagents *config.SubagentsConfig
|
|
|
|
|
var skillsFilter []string
|
|
|
|
|
|
|
|
|
|
if agentCfg != nil {
|
|
|
|
|
agentID = routing.NormalizeAgentID(agentCfg.ID)
|
|
|
|
|
agentName = agentCfg.Name
|
|
|
|
|
subagents = agentCfg.Subagents
|
|
|
|
|
skillsFilter = agentCfg.Skills
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
maxIter := defaults.MaxToolIterations
|
|
|
|
|
if maxIter == 0 {
|
|
|
|
|
maxIter = 20
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 18:16:37 +00:00
|
|
|
maxTokens := defaults.MaxTokens
|
|
|
|
|
if maxTokens == 0 {
|
|
|
|
|
maxTokens = 8192
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
temperature := 0.7
|
|
|
|
|
if defaults.Temperature != nil {
|
|
|
|
|
temperature = *defaults.Temperature
|
|
|
|
|
}
|
|
|
|
|
|
feat: add multi-agent routing with declarative bindings
Implement per-agent workspace/model/session isolation with 7-level
priority routing cascade (peer > parent_peer > guild > team > account >
channel > default). Backward compatible - empty agents.list creates
implicit "main" agent from defaults.
Core components:
- routing/agent_id.go: ID normalization with pre-compiled regex
- routing/session_key.go: 4 DM scope modes with identity links
- routing/route.go: RouteResolver with priority-based binding matcher
- agent/instance.go: Per-agent state (workspace, sessions, tools, model)
- agent/registry.go: Agent lifecycle, route resolution, subagent ACL
Integration:
- config.go: AgentModelConfig (flexible JSON), bindings, session config
- loop.go: Complete rewrite for multi-agent dispatch
- Channel adapters: peer_kind/peer_id metadata (telegram, discord, slack)
- spawn.go: Subagent allowlist enforcement per agent
Validated end-to-end with Discord channel-based bindings, default
fallback routing, and per-agent session persistence.
2026-02-13 15:12:33 +00:00
|
|
|
// Resolve fallback candidates
|
|
|
|
|
modelCfg := providers.ModelConfig{
|
|
|
|
|
Primary: model,
|
|
|
|
|
Fallbacks: fallbacks,
|
|
|
|
|
}
|
|
|
|
|
candidates := providers.ResolveCandidates(modelCfg, defaults.Provider)
|
|
|
|
|
|
|
|
|
|
return &AgentInstance{
|
|
|
|
|
ID: agentID,
|
|
|
|
|
Name: agentName,
|
|
|
|
|
Model: model,
|
|
|
|
|
Fallbacks: fallbacks,
|
|
|
|
|
Workspace: workspace,
|
|
|
|
|
MaxIterations: maxIter,
|
2026-02-19 18:16:37 +00:00
|
|
|
MaxTokens: maxTokens,
|
|
|
|
|
Temperature: temperature,
|
|
|
|
|
ContextWindow: maxTokens,
|
feat: add multi-agent routing with declarative bindings
Implement per-agent workspace/model/session isolation with 7-level
priority routing cascade (peer > parent_peer > guild > team > account >
channel > default). Backward compatible - empty agents.list creates
implicit "main" agent from defaults.
Core components:
- routing/agent_id.go: ID normalization with pre-compiled regex
- routing/session_key.go: 4 DM scope modes with identity links
- routing/route.go: RouteResolver with priority-based binding matcher
- agent/instance.go: Per-agent state (workspace, sessions, tools, model)
- agent/registry.go: Agent lifecycle, route resolution, subagent ACL
Integration:
- config.go: AgentModelConfig (flexible JSON), bindings, session config
- loop.go: Complete rewrite for multi-agent dispatch
- Channel adapters: peer_kind/peer_id metadata (telegram, discord, slack)
- spawn.go: Subagent allowlist enforcement per agent
Validated end-to-end with Discord channel-based bindings, default
fallback routing, and per-agent session persistence.
2026-02-13 15:12:33 +00:00
|
|
|
Provider: provider,
|
|
|
|
|
Sessions: sessionsManager,
|
|
|
|
|
ContextBuilder: contextBuilder,
|
|
|
|
|
Tools: toolsRegistry,
|
|
|
|
|
Subagents: subagents,
|
|
|
|
|
SkillsFilter: skillsFilter,
|
|
|
|
|
Candidates: candidates,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// resolveAgentWorkspace determines the workspace directory for an agent.
|
|
|
|
|
func resolveAgentWorkspace(agentCfg *config.AgentConfig, defaults *config.AgentDefaults) string {
|
|
|
|
|
if agentCfg != nil && strings.TrimSpace(agentCfg.Workspace) != "" {
|
|
|
|
|
return expandHome(strings.TrimSpace(agentCfg.Workspace))
|
|
|
|
|
}
|
|
|
|
|
if agentCfg == nil || agentCfg.Default || agentCfg.ID == "" || routing.NormalizeAgentID(agentCfg.ID) == "main" {
|
|
|
|
|
return expandHome(defaults.Workspace)
|
|
|
|
|
}
|
|
|
|
|
home, _ := os.UserHomeDir()
|
|
|
|
|
id := routing.NormalizeAgentID(agentCfg.ID)
|
|
|
|
|
return filepath.Join(home, ".picoclaw", "workspace-"+id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// resolveAgentModel resolves the primary model for an agent.
|
|
|
|
|
func resolveAgentModel(agentCfg *config.AgentConfig, defaults *config.AgentDefaults) string {
|
|
|
|
|
if agentCfg != nil && agentCfg.Model != nil && strings.TrimSpace(agentCfg.Model.Primary) != "" {
|
|
|
|
|
return strings.TrimSpace(agentCfg.Model.Primary)
|
|
|
|
|
}
|
2026-02-23 08:55:06 +00:00
|
|
|
return defaults.GetModelName()
|
feat: add multi-agent routing with declarative bindings
Implement per-agent workspace/model/session isolation with 7-level
priority routing cascade (peer > parent_peer > guild > team > account >
channel > default). Backward compatible - empty agents.list creates
implicit "main" agent from defaults.
Core components:
- routing/agent_id.go: ID normalization with pre-compiled regex
- routing/session_key.go: 4 DM scope modes with identity links
- routing/route.go: RouteResolver with priority-based binding matcher
- agent/instance.go: Per-agent state (workspace, sessions, tools, model)
- agent/registry.go: Agent lifecycle, route resolution, subagent ACL
Integration:
- config.go: AgentModelConfig (flexible JSON), bindings, session config
- loop.go: Complete rewrite for multi-agent dispatch
- Channel adapters: peer_kind/peer_id metadata (telegram, discord, slack)
- spawn.go: Subagent allowlist enforcement per agent
Validated end-to-end with Discord channel-based bindings, default
fallback routing, and per-agent session persistence.
2026-02-13 15:12:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// resolveAgentFallbacks resolves the fallback models for an agent.
|
|
|
|
|
func resolveAgentFallbacks(agentCfg *config.AgentConfig, defaults *config.AgentDefaults) []string {
|
|
|
|
|
if agentCfg != nil && agentCfg.Model != nil && agentCfg.Model.Fallbacks != nil {
|
|
|
|
|
return agentCfg.Model.Fallbacks
|
|
|
|
|
}
|
|
|
|
|
return defaults.ModelFallbacks
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func expandHome(path string) string {
|
|
|
|
|
if path == "" {
|
|
|
|
|
return path
|
|
|
|
|
}
|
|
|
|
|
if path[0] == '~' {
|
|
|
|
|
home, _ := os.UserHomeDir()
|
|
|
|
|
if len(path) > 1 && path[1] == '/' {
|
|
|
|
|
return home + path[1:]
|
|
|
|
|
}
|
|
|
|
|
return home
|
|
|
|
|
}
|
|
|
|
|
return path
|
|
|
|
|
}
|