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
|
|
|
|
|
|
2026-02-27 13:20:56 +00:00
|
|
|
import (
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
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 TestNormalizeAgentID_Empty(t *testing.T) {
|
|
|
|
|
if got := NormalizeAgentID(""); got != DefaultAgentID {
|
|
|
|
|
t.Errorf("NormalizeAgentID('') = %q, want %q", got, DefaultAgentID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNormalizeAgentID_Whitespace(t *testing.T) {
|
|
|
|
|
if got := NormalizeAgentID(" "); got != DefaultAgentID {
|
|
|
|
|
t.Errorf("NormalizeAgentID(' ') = %q, want %q", got, DefaultAgentID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNormalizeAgentID_Valid(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
input, want string
|
|
|
|
|
}{
|
|
|
|
|
{"main", "main"},
|
|
|
|
|
{"Main", "main"},
|
|
|
|
|
{"SALES", "sales"},
|
|
|
|
|
{"support-bot", "support-bot"},
|
|
|
|
|
{"agent_1", "agent_1"},
|
|
|
|
|
{"a", "a"},
|
|
|
|
|
{"0test", "0test"},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
if got := NormalizeAgentID(tt.input); got != tt.want {
|
|
|
|
|
t.Errorf("NormalizeAgentID(%q) = %q, want %q", tt.input, got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNormalizeAgentID_InvalidChars(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
input, want string
|
|
|
|
|
}{
|
|
|
|
|
{"Hello World", "hello-world"},
|
|
|
|
|
{"agent@123", "agent-123"},
|
|
|
|
|
{"foo.bar.baz", "foo-bar-baz"},
|
|
|
|
|
{"--leading", "leading"},
|
|
|
|
|
{"--both--", "both"},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
if got := NormalizeAgentID(tt.input); got != tt.want {
|
|
|
|
|
t.Errorf("NormalizeAgentID(%q) = %q, want %q", tt.input, got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNormalizeAgentID_AllInvalid(t *testing.T) {
|
|
|
|
|
if got := NormalizeAgentID("@@@"); got != DefaultAgentID {
|
|
|
|
|
t.Errorf("NormalizeAgentID('@@@') = %q, want %q", got, DefaultAgentID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNormalizeAgentID_TruncatesAt64(t *testing.T) {
|
2026-02-27 08:35:07 +00:00
|
|
|
var long strings.Builder
|
|
|
|
|
for range 100 {
|
|
|
|
|
long.WriteString("a")
|
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
|
|
|
}
|
2026-02-27 08:35:07 +00:00
|
|
|
got := NormalizeAgentID(long.String())
|
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 len(got) > MaxAgentIDLength {
|
|
|
|
|
t.Errorf("length = %d, want <= %d", len(got), MaxAgentIDLength)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNormalizeAccountID_Empty(t *testing.T) {
|
|
|
|
|
if got := NormalizeAccountID(""); got != DefaultAccountID {
|
|
|
|
|
t.Errorf("NormalizeAccountID('') = %q, want %q", got, DefaultAccountID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNormalizeAccountID_Valid(t *testing.T) {
|
|
|
|
|
if got := NormalizeAccountID("MyBot"); got != "mybot" {
|
|
|
|
|
t.Errorf("NormalizeAccountID('MyBot') = %q, want 'mybot'", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNormalizeAccountID_InvalidChars(t *testing.T) {
|
|
|
|
|
if got := NormalizeAccountID("bot@home"); got != "bot-home" {
|
|
|
|
|
t.Errorf("NormalizeAccountID('bot@home') = %q, want 'bot-home'", got)
|
|
|
|
|
}
|
|
|
|
|
}
|