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 routing
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"testing"
|
|
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
"github.com/sipeed/picoclaw/pkg/bus"
|
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
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func testConfig(agents []config.AgentConfig, bindings []config.AgentBinding) *config.Config {
|
|
|
|
|
return &config.Config{
|
|
|
|
|
Agents: config.AgentsConfig{
|
|
|
|
|
Defaults: config.AgentDefaults{
|
|
|
|
|
Workspace: "/tmp/picoclaw-test",
|
2026-03-11 08:33:01 +00:00
|
|
|
ModelName: "gpt-4",
|
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
|
|
|
},
|
|
|
|
|
List: agents,
|
|
|
|
|
},
|
|
|
|
|
Bindings: bindings,
|
|
|
|
|
Session: config.SessionConfig{
|
2026-04-01 09:19:50 +00:00
|
|
|
Dimensions: []string{"sender"},
|
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
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveRoute_DefaultAgent_NoBindings(t *testing.T) {
|
|
|
|
|
cfg := testConfig(nil, nil)
|
|
|
|
|
r := NewRouteResolver(cfg)
|
|
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
route := r.ResolveRoute(bus.InboundContext{
|
|
|
|
|
Channel: "telegram",
|
|
|
|
|
ChatType: "direct",
|
|
|
|
|
SenderID: "user1",
|
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
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if route.AgentID != DefaultAgentID {
|
|
|
|
|
t.Errorf("AgentID = %q, want %q", route.AgentID, DefaultAgentID)
|
|
|
|
|
}
|
|
|
|
|
if route.MatchedBy != "default" {
|
|
|
|
|
t.Errorf("MatchedBy = %q, want 'default'", route.MatchedBy)
|
|
|
|
|
}
|
2026-04-01 09:19:50 +00:00
|
|
|
if len(route.SessionPolicy.Dimensions) != 1 || route.SessionPolicy.Dimensions[0] != "sender" {
|
|
|
|
|
t.Errorf("SessionPolicy.Dimensions = %v, want [sender]", route.SessionPolicy.Dimensions)
|
2026-04-01 06:26:12 +00:00
|
|
|
}
|
|
|
|
|
if route.SessionPolicy.IdentityLinks != nil {
|
|
|
|
|
t.Errorf("SessionPolicy.IdentityLinks = %v, want nil", route.SessionPolicy.IdentityLinks)
|
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveRoute_PeerBinding(t *testing.T) {
|
|
|
|
|
agents := []config.AgentConfig{
|
|
|
|
|
{ID: "sales", Default: true},
|
|
|
|
|
{ID: "support"},
|
|
|
|
|
}
|
|
|
|
|
bindings := []config.AgentBinding{
|
|
|
|
|
{
|
|
|
|
|
AgentID: "support",
|
|
|
|
|
Match: config.BindingMatch{
|
|
|
|
|
Channel: "telegram",
|
|
|
|
|
AccountID: "*",
|
|
|
|
|
Peer: &config.PeerMatch{Kind: "direct", ID: "user123"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
cfg := testConfig(agents, bindings)
|
|
|
|
|
r := NewRouteResolver(cfg)
|
|
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
route := r.ResolveRoute(bus.InboundContext{
|
|
|
|
|
Channel: "telegram",
|
|
|
|
|
ChatType: "direct",
|
|
|
|
|
SenderID: "user123",
|
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
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if route.AgentID != "support" {
|
|
|
|
|
t.Errorf("AgentID = %q, want 'support'", route.AgentID)
|
|
|
|
|
}
|
|
|
|
|
if route.MatchedBy != "binding.peer" {
|
|
|
|
|
t.Errorf("MatchedBy = %q, want 'binding.peer'", route.MatchedBy)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveRoute_GuildBinding(t *testing.T) {
|
|
|
|
|
agents := []config.AgentConfig{
|
|
|
|
|
{ID: "general", Default: true},
|
|
|
|
|
{ID: "gaming"},
|
|
|
|
|
}
|
|
|
|
|
bindings := []config.AgentBinding{
|
|
|
|
|
{
|
|
|
|
|
AgentID: "gaming",
|
|
|
|
|
Match: config.BindingMatch{
|
|
|
|
|
Channel: "discord",
|
|
|
|
|
AccountID: "*",
|
|
|
|
|
GuildID: "guild-abc",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
cfg := testConfig(agents, bindings)
|
|
|
|
|
r := NewRouteResolver(cfg)
|
|
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
route := r.ResolveRoute(bus.InboundContext{
|
|
|
|
|
Channel: "discord",
|
|
|
|
|
ChatID: "ch1",
|
|
|
|
|
ChatType: "channel",
|
|
|
|
|
SpaceID: "guild-abc",
|
|
|
|
|
SpaceType: "guild",
|
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
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if route.AgentID != "gaming" {
|
|
|
|
|
t.Errorf("AgentID = %q, want 'gaming'", route.AgentID)
|
|
|
|
|
}
|
|
|
|
|
if route.MatchedBy != "binding.guild" {
|
|
|
|
|
t.Errorf("MatchedBy = %q, want 'binding.guild'", route.MatchedBy)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveRoute_TeamBinding(t *testing.T) {
|
|
|
|
|
agents := []config.AgentConfig{
|
|
|
|
|
{ID: "general", Default: true},
|
|
|
|
|
{ID: "work"},
|
|
|
|
|
}
|
|
|
|
|
bindings := []config.AgentBinding{
|
|
|
|
|
{
|
|
|
|
|
AgentID: "work",
|
|
|
|
|
Match: config.BindingMatch{
|
|
|
|
|
Channel: "slack",
|
|
|
|
|
AccountID: "*",
|
|
|
|
|
TeamID: "T12345",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
cfg := testConfig(agents, bindings)
|
|
|
|
|
r := NewRouteResolver(cfg)
|
|
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
route := r.ResolveRoute(bus.InboundContext{
|
|
|
|
|
Channel: "slack",
|
|
|
|
|
ChatID: "C001",
|
|
|
|
|
ChatType: "channel",
|
|
|
|
|
SpaceID: "T12345",
|
|
|
|
|
SpaceType: "team",
|
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
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if route.AgentID != "work" {
|
|
|
|
|
t.Errorf("AgentID = %q, want 'work'", route.AgentID)
|
|
|
|
|
}
|
|
|
|
|
if route.MatchedBy != "binding.team" {
|
|
|
|
|
t.Errorf("MatchedBy = %q, want 'binding.team'", route.MatchedBy)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveRoute_AccountBinding(t *testing.T) {
|
|
|
|
|
agents := []config.AgentConfig{
|
|
|
|
|
{ID: "default-agent", Default: true},
|
|
|
|
|
{ID: "premium"},
|
|
|
|
|
}
|
|
|
|
|
bindings := []config.AgentBinding{
|
|
|
|
|
{
|
|
|
|
|
AgentID: "premium",
|
|
|
|
|
Match: config.BindingMatch{
|
|
|
|
|
Channel: "telegram",
|
|
|
|
|
AccountID: "bot2",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
cfg := testConfig(agents, bindings)
|
|
|
|
|
r := NewRouteResolver(cfg)
|
|
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
route := r.ResolveRoute(bus.InboundContext{
|
|
|
|
|
Channel: "telegram",
|
|
|
|
|
Account: "bot2",
|
|
|
|
|
ChatType: "direct",
|
|
|
|
|
SenderID: "user1",
|
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
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if route.AgentID != "premium" {
|
|
|
|
|
t.Errorf("AgentID = %q, want 'premium'", route.AgentID)
|
|
|
|
|
}
|
|
|
|
|
if route.MatchedBy != "binding.account" {
|
|
|
|
|
t.Errorf("MatchedBy = %q, want 'binding.account'", route.MatchedBy)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveRoute_ChannelWildcard(t *testing.T) {
|
|
|
|
|
agents := []config.AgentConfig{
|
|
|
|
|
{ID: "main", Default: true},
|
|
|
|
|
{ID: "telegram-bot"},
|
|
|
|
|
}
|
|
|
|
|
bindings := []config.AgentBinding{
|
|
|
|
|
{
|
|
|
|
|
AgentID: "telegram-bot",
|
|
|
|
|
Match: config.BindingMatch{
|
|
|
|
|
Channel: "telegram",
|
|
|
|
|
AccountID: "*",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
cfg := testConfig(agents, bindings)
|
|
|
|
|
r := NewRouteResolver(cfg)
|
|
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
route := r.ResolveRoute(bus.InboundContext{
|
|
|
|
|
Channel: "telegram",
|
|
|
|
|
ChatType: "direct",
|
|
|
|
|
SenderID: "user1",
|
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
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if route.AgentID != "telegram-bot" {
|
|
|
|
|
t.Errorf("AgentID = %q, want 'telegram-bot'", route.AgentID)
|
|
|
|
|
}
|
|
|
|
|
if route.MatchedBy != "binding.channel" {
|
|
|
|
|
t.Errorf("MatchedBy = %q, want 'binding.channel'", route.MatchedBy)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveRoute_PriorityOrder_PeerBeatsGuild(t *testing.T) {
|
|
|
|
|
agents := []config.AgentConfig{
|
|
|
|
|
{ID: "general", Default: true},
|
|
|
|
|
{ID: "vip"},
|
|
|
|
|
{ID: "gaming"},
|
|
|
|
|
}
|
|
|
|
|
bindings := []config.AgentBinding{
|
|
|
|
|
{
|
|
|
|
|
AgentID: "vip",
|
|
|
|
|
Match: config.BindingMatch{
|
|
|
|
|
Channel: "discord",
|
|
|
|
|
AccountID: "*",
|
|
|
|
|
Peer: &config.PeerMatch{Kind: "direct", ID: "user-vip"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
AgentID: "gaming",
|
|
|
|
|
Match: config.BindingMatch{
|
|
|
|
|
Channel: "discord",
|
|
|
|
|
AccountID: "*",
|
|
|
|
|
GuildID: "guild-1",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
cfg := testConfig(agents, bindings)
|
|
|
|
|
r := NewRouteResolver(cfg)
|
|
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
route := r.ResolveRoute(bus.InboundContext{
|
|
|
|
|
Channel: "discord",
|
|
|
|
|
ChatType: "direct",
|
|
|
|
|
SenderID: "user-vip",
|
|
|
|
|
SpaceID: "guild-1",
|
|
|
|
|
SpaceType: "guild",
|
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
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if route.AgentID != "vip" {
|
|
|
|
|
t.Errorf("AgentID = %q, want 'vip' (peer should beat guild)", route.AgentID)
|
|
|
|
|
}
|
|
|
|
|
if route.MatchedBy != "binding.peer" {
|
|
|
|
|
t.Errorf("MatchedBy = %q, want 'binding.peer'", route.MatchedBy)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveRoute_InvalidAgentFallsToDefault(t *testing.T) {
|
|
|
|
|
agents := []config.AgentConfig{
|
|
|
|
|
{ID: "main", Default: true},
|
|
|
|
|
}
|
|
|
|
|
bindings := []config.AgentBinding{
|
|
|
|
|
{
|
|
|
|
|
AgentID: "nonexistent",
|
|
|
|
|
Match: config.BindingMatch{
|
|
|
|
|
Channel: "telegram",
|
|
|
|
|
AccountID: "*",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
cfg := testConfig(agents, bindings)
|
|
|
|
|
r := NewRouteResolver(cfg)
|
|
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
route := r.ResolveRoute(bus.InboundContext{Channel: "telegram"})
|
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
|
|
|
|
|
|
|
|
if route.AgentID != "main" {
|
|
|
|
|
t.Errorf("AgentID = %q, want 'main' (invalid agent should fall to default)", route.AgentID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveRoute_DefaultAgentSelection(t *testing.T) {
|
|
|
|
|
agents := []config.AgentConfig{
|
|
|
|
|
{ID: "alpha"},
|
|
|
|
|
{ID: "beta", Default: true},
|
|
|
|
|
{ID: "gamma"},
|
|
|
|
|
}
|
|
|
|
|
cfg := testConfig(agents, nil)
|
|
|
|
|
r := NewRouteResolver(cfg)
|
|
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
route := r.ResolveRoute(bus.InboundContext{Channel: "cli"})
|
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
|
|
|
|
|
|
|
|
if route.AgentID != "beta" {
|
|
|
|
|
t.Errorf("AgentID = %q, want 'beta' (marked as default)", route.AgentID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveRoute_NoDefaultUsesFirst(t *testing.T) {
|
|
|
|
|
agents := []config.AgentConfig{
|
|
|
|
|
{ID: "alpha"},
|
|
|
|
|
{ID: "beta"},
|
|
|
|
|
}
|
|
|
|
|
cfg := testConfig(agents, nil)
|
|
|
|
|
r := NewRouteResolver(cfg)
|
|
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
route := r.ResolveRoute(bus.InboundContext{Channel: "cli"})
|
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
|
|
|
|
|
|
|
|
if route.AgentID != "alpha" {
|
|
|
|
|
t.Errorf("AgentID = %q, want 'alpha' (first in list)", route.AgentID)
|
|
|
|
|
}
|
|
|
|
|
}
|