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 (
|
feat(session): integrate JSONL persistence into agent loop (#1170)
* feat(session): add SessionStore interface and JSONL backend adapter
Extract a SessionStore interface from the methods the agent loop uses
(AddMessage, GetHistory, SetSummary, TruncateHistory, Save, etc.).
Both SessionManager and the new JSONLBackend satisfy this interface,
allowing the persistence layer to be swapped transparently.
JSONLBackend wraps memory.Store and maps its error-returning API to
the fire-and-forget contract that the agent loop expects — write
errors are logged, reads return empty defaults on failure. Save()
triggers compaction to reclaim space after logical truncation.
Part of #1169
* test(session): add JSONLBackend integration tests
8 tests covering the full SessionStore contract through the JSONL
backend: message roundtrip, tool calls, summary, truncation with
compaction, history replacement, empty sessions, session isolation,
and the complete summarization flow (SetSummary → TruncateHistory →
Save).
Includes compile-time interface satisfaction checks for both
SessionManager and JSONLBackend.
Part of #1169
* feat(agent): wire JSONL session store into agent loop
Replace the concrete *SessionManager field with the SessionStore
interface and initialize the JSONL backend by default. Legacy .json
session files are auto-migrated on first startup. Falls back to
SessionManager if the JSONL store cannot be initialized.
The agent loop code (loop.go) requires zero changes — all method
calls work identically through the interface.
Closes #1169
* fix(session): propagate compact error from Save
Save() was swallowing the error returned by Compact and always
returning nil. Callers checking Save's return value would never
see a compaction failure. Return the error directly so the agent
loop can log or handle it as needed.
* feat(session): add Close to SessionStore interface
Add Close() error to SessionStore so callers can release resources
through the interface. JSONLBackend already had Close; this adds
a no-op implementation to SessionManager for compatibility.
* fix(session): close session stores on shutdown and harden migration
- Add Close() to AgentInstance, AgentRegistry, and AgentLoop so JSONL
file handles are released during gateway shutdown and CLI exit.
- Fall back to SessionManager when migration fails, preventing a split
state where some sessions live in JSONL and others remain in JSON.
- Add defer agentLoop.Close() in the CLI agent command path.
- Document SessionStore interface methods (fire-and-forget contract).
2026-03-10 07:14:09 +00:00
|
|
|
"context"
|
fix(tools): allow /dev/null redirection and add read/write sandbox split (#967)
* fix(tools): allow /dev/null redirection and add read/write sandbox split
- Remove deny pattern that incorrectly blocked redirects to /dev/null
- Expand block device write pattern to cover nvme, mmcblk, vd, xvd,
hd, loop, dm-, md, sr and nbd in addition to sd
- Add safe path whitelist for kernel pseudo-devices so workspace path
check does not reject /dev/null, /dev/zero, /dev/random, /dev/urandom,
/dev/stdin, /dev/stdout and /dev/stderr
- Add allow_read_outside_workspace config option (default true) so file
read and list tools are unrestricted while write tools stay sandboxed
Closes https://github.com/sipeed/picoclaw/issues/964
Closes https://github.com/sipeed/picoclaw/issues/965
Signed-off-by: Huang Rui <vowstar@gmail.com>
* feat(tools): add configurable allow patterns and path whitelists
- Add custom_allow_patterns to exec config so users can exempt specific
commands from deny pattern checks
- Add allow_read_paths and allow_write_paths regex lists to tools config
for whitelisting specific paths outside the workspace
- Introduce whitelistFs that wraps sandboxFs and falls through to hostFs
for paths matching whitelist patterns
- Use variadic constructor signatures to keep backward compatibility
Suggested-by: lxowalle
Signed-off-by: Huang Rui <vowstar@gmail.com>
---------
Signed-off-by: Huang Rui <vowstar@gmail.com>
2026-03-02 04:22:02 +00:00
|
|
|
"fmt"
|
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
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
fix(tools): allow /dev/null redirection and add read/write sandbox split (#967)
* fix(tools): allow /dev/null redirection and add read/write sandbox split
- Remove deny pattern that incorrectly blocked redirects to /dev/null
- Expand block device write pattern to cover nvme, mmcblk, vd, xvd,
hd, loop, dm-, md, sr and nbd in addition to sd
- Add safe path whitelist for kernel pseudo-devices so workspace path
check does not reject /dev/null, /dev/zero, /dev/random, /dev/urandom,
/dev/stdin, /dev/stdout and /dev/stderr
- Add allow_read_outside_workspace config option (default true) so file
read and list tools are unrestricted while write tools stay sandboxed
Closes https://github.com/sipeed/picoclaw/issues/964
Closes https://github.com/sipeed/picoclaw/issues/965
Signed-off-by: Huang Rui <vowstar@gmail.com>
* feat(tools): add configurable allow patterns and path whitelists
- Add custom_allow_patterns to exec config so users can exempt specific
commands from deny pattern checks
- Add allow_read_paths and allow_write_paths regex lists to tools config
for whitelisting specific paths outside the workspace
- Introduce whitelistFs that wraps sandboxFs and falls through to hostFs
for paths matching whitelist patterns
- Use variadic constructor signatures to keep backward compatibility
Suggested-by: lxowalle
Signed-off-by: Huang Rui <vowstar@gmail.com>
---------
Signed-off-by: Huang Rui <vowstar@gmail.com>
2026-03-02 04:22:02 +00:00
|
|
|
"regexp"
|
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
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
2026-03-19 13:11:36 +00:00
|
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
2026-03-14 04:02:06 +00:00
|
|
|
"github.com/sipeed/picoclaw/pkg/media"
|
feat(session): integrate JSONL persistence into agent loop (#1170)
* feat(session): add SessionStore interface and JSONL backend adapter
Extract a SessionStore interface from the methods the agent loop uses
(AddMessage, GetHistory, SetSummary, TruncateHistory, Save, etc.).
Both SessionManager and the new JSONLBackend satisfy this interface,
allowing the persistence layer to be swapped transparently.
JSONLBackend wraps memory.Store and maps its error-returning API to
the fire-and-forget contract that the agent loop expects — write
errors are logged, reads return empty defaults on failure. Save()
triggers compaction to reclaim space after logical truncation.
Part of #1169
* test(session): add JSONLBackend integration tests
8 tests covering the full SessionStore contract through the JSONL
backend: message roundtrip, tool calls, summary, truncation with
compaction, history replacement, empty sessions, session isolation,
and the complete summarization flow (SetSummary → TruncateHistory →
Save).
Includes compile-time interface satisfaction checks for both
SessionManager and JSONLBackend.
Part of #1169
* feat(agent): wire JSONL session store into agent loop
Replace the concrete *SessionManager field with the SessionStore
interface and initialize the JSONL backend by default. Legacy .json
session files are auto-migrated on first startup. Falls back to
SessionManager if the JSONL store cannot be initialized.
The agent loop code (loop.go) requires zero changes — all method
calls work identically through the interface.
Closes #1169
* fix(session): propagate compact error from Save
Save() was swallowing the error returned by Compact and always
returning nil. Callers checking Save's return value would never
see a compaction failure. Return the error directly so the agent
loop can log or handle it as needed.
* feat(session): add Close to SessionStore interface
Add Close() error to SessionStore so callers can release resources
through the interface. JSONLBackend already had Close; this adds
a no-op implementation to SessionManager for compatibility.
* fix(session): close session stores on shutdown and harden migration
- Add Close() to AgentInstance, AgentRegistry, and AgentLoop so JSONL
file handles are released during gateway shutdown and CLI exit.
- Fall back to SessionManager when migration fails, preventing a split
state where some sessions live in JSONL and others remain in JSON.
- Add defer agentLoop.Close() in the CLI agent command path.
- Document SessionStore interface methods (fire-and-forget contract).
2026-03-10 07:14:09 +00:00
|
|
|
"github.com/sipeed/picoclaw/pkg/memory"
|
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/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 {
|
2026-03-04 03:23:01 +00:00
|
|
|
ID string
|
|
|
|
|
Name string
|
|
|
|
|
Model string
|
|
|
|
|
Fallbacks []string
|
|
|
|
|
Workspace string
|
|
|
|
|
MaxIterations int
|
|
|
|
|
MaxTokens int
|
|
|
|
|
Temperature float64
|
2026-03-05 01:51:18 +00:00
|
|
|
ThinkingLevel ThinkingLevel
|
2026-03-04 03:23:01 +00:00
|
|
|
ContextWindow int
|
|
|
|
|
SummarizeMessageThreshold int
|
|
|
|
|
SummarizeTokenPercent int
|
|
|
|
|
Provider providers.LLMProvider
|
feat(session): integrate JSONL persistence into agent loop (#1170)
* feat(session): add SessionStore interface and JSONL backend adapter
Extract a SessionStore interface from the methods the agent loop uses
(AddMessage, GetHistory, SetSummary, TruncateHistory, Save, etc.).
Both SessionManager and the new JSONLBackend satisfy this interface,
allowing the persistence layer to be swapped transparently.
JSONLBackend wraps memory.Store and maps its error-returning API to
the fire-and-forget contract that the agent loop expects — write
errors are logged, reads return empty defaults on failure. Save()
triggers compaction to reclaim space after logical truncation.
Part of #1169
* test(session): add JSONLBackend integration tests
8 tests covering the full SessionStore contract through the JSONL
backend: message roundtrip, tool calls, summary, truncation with
compaction, history replacement, empty sessions, session isolation,
and the complete summarization flow (SetSummary → TruncateHistory →
Save).
Includes compile-time interface satisfaction checks for both
SessionManager and JSONLBackend.
Part of #1169
* feat(agent): wire JSONL session store into agent loop
Replace the concrete *SessionManager field with the SessionStore
interface and initialize the JSONL backend by default. Legacy .json
session files are auto-migrated on first startup. Falls back to
SessionManager if the JSONL store cannot be initialized.
The agent loop code (loop.go) requires zero changes — all method
calls work identically through the interface.
Closes #1169
* fix(session): propagate compact error from Save
Save() was swallowing the error returned by Compact and always
returning nil. Callers checking Save's return value would never
see a compaction failure. Return the error directly so the agent
loop can log or handle it as needed.
* feat(session): add Close to SessionStore interface
Add Close() error to SessionStore so callers can release resources
through the interface. JSONLBackend already had Close; this adds
a no-op implementation to SessionManager for compatibility.
* fix(session): close session stores on shutdown and harden migration
- Add Close() to AgentInstance, AgentRegistry, and AgentLoop so JSONL
file handles are released during gateway shutdown and CLI exit.
- Fall back to SessionManager when migration fails, preventing a split
state where some sessions live in JSONL and others remain in JSON.
- Add defer agentLoop.Close() in the CLI agent command path.
- Document SessionStore interface methods (fire-and-forget contract).
2026-03-10 07:14:09 +00:00
|
|
|
Sessions session.SessionStore
|
2026-03-04 03:23:01 +00:00
|
|
|
ContextBuilder *ContextBuilder
|
|
|
|
|
Tools *tools.ToolRegistry
|
|
|
|
|
Subagents *config.SubagentsConfig
|
|
|
|
|
SkillsFilter []string
|
|
|
|
|
Candidates []providers.FallbackCandidate
|
2026-03-02 14:42:52 +00:00
|
|
|
|
|
|
|
|
// Router is non-nil when model routing is configured and the light model
|
|
|
|
|
// was successfully resolved. It scores each incoming message and decides
|
|
|
|
|
// whether to route to LightCandidates or stay with Candidates.
|
|
|
|
|
Router *routing.Router
|
|
|
|
|
// LightCandidates holds the resolved provider candidates for the light model.
|
|
|
|
|
// Pre-computed at agent creation to avoid repeated model_list lookups at runtime.
|
|
|
|
|
LightCandidates []providers.FallbackCandidate
|
2026-03-28 07:25:23 +00:00
|
|
|
// LightProvider is the concrete provider instance for the configured light model.
|
|
|
|
|
// It is only used when routing selects the light tier for a turn.
|
|
|
|
|
LightProvider providers.LLMProvider
|
2026-04-07 12:07:56 +00:00
|
|
|
// CandidateProviders maps "provider/model" keys to per-candidate LLMProvider
|
|
|
|
|
// instances. This allows each fallback model to use its own api_base and api_key
|
|
|
|
|
// from model_list, instead of inheriting the primary model's provider config.
|
|
|
|
|
CandidateProviders map[string]providers.LLMProvider
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
fix(tools): allow /dev/null redirection and add read/write sandbox split (#967)
* fix(tools): allow /dev/null redirection and add read/write sandbox split
- Remove deny pattern that incorrectly blocked redirects to /dev/null
- Expand block device write pattern to cover nvme, mmcblk, vd, xvd,
hd, loop, dm-, md, sr and nbd in addition to sd
- Add safe path whitelist for kernel pseudo-devices so workspace path
check does not reject /dev/null, /dev/zero, /dev/random, /dev/urandom,
/dev/stdin, /dev/stdout and /dev/stderr
- Add allow_read_outside_workspace config option (default true) so file
read and list tools are unrestricted while write tools stay sandboxed
Closes https://github.com/sipeed/picoclaw/issues/964
Closes https://github.com/sipeed/picoclaw/issues/965
Signed-off-by: Huang Rui <vowstar@gmail.com>
* feat(tools): add configurable allow patterns and path whitelists
- Add custom_allow_patterns to exec config so users can exempt specific
commands from deny pattern checks
- Add allow_read_paths and allow_write_paths regex lists to tools config
for whitelisting specific paths outside the workspace
- Introduce whitelistFs that wraps sandboxFs and falls through to hostFs
for paths matching whitelist patterns
- Use variadic constructor signatures to keep backward compatibility
Suggested-by: lxowalle
Signed-off-by: Huang Rui <vowstar@gmail.com>
---------
Signed-off-by: Huang Rui <vowstar@gmail.com>
2026-03-02 04:22:02 +00:00
|
|
|
readRestrict := restrict && !defaults.AllowReadOutsideWorkspace
|
|
|
|
|
|
|
|
|
|
// Compile path whitelist patterns from config.
|
2026-03-14 04:02:06 +00:00
|
|
|
allowReadPaths := buildAllowReadPatterns(cfg)
|
fix(tools): allow /dev/null redirection and add read/write sandbox split (#967)
* fix(tools): allow /dev/null redirection and add read/write sandbox split
- Remove deny pattern that incorrectly blocked redirects to /dev/null
- Expand block device write pattern to cover nvme, mmcblk, vd, xvd,
hd, loop, dm-, md, sr and nbd in addition to sd
- Add safe path whitelist for kernel pseudo-devices so workspace path
check does not reject /dev/null, /dev/zero, /dev/random, /dev/urandom,
/dev/stdin, /dev/stdout and /dev/stderr
- Add allow_read_outside_workspace config option (default true) so file
read and list tools are unrestricted while write tools stay sandboxed
Closes https://github.com/sipeed/picoclaw/issues/964
Closes https://github.com/sipeed/picoclaw/issues/965
Signed-off-by: Huang Rui <vowstar@gmail.com>
* feat(tools): add configurable allow patterns and path whitelists
- Add custom_allow_patterns to exec config so users can exempt specific
commands from deny pattern checks
- Add allow_read_paths and allow_write_paths regex lists to tools config
for whitelisting specific paths outside the workspace
- Introduce whitelistFs that wraps sandboxFs and falls through to hostFs
for paths matching whitelist patterns
- Use variadic constructor signatures to keep backward compatibility
Suggested-by: lxowalle
Signed-off-by: Huang Rui <vowstar@gmail.com>
---------
Signed-off-by: Huang Rui <vowstar@gmail.com>
2026-03-02 04:22:02 +00:00
|
|
|
allowWritePaths := compilePatterns(cfg.Tools.AllowWritePaths)
|
|
|
|
|
|
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 := tools.NewToolRegistry()
|
2026-03-05 06:53:26 +00:00
|
|
|
|
|
|
|
|
if cfg.Tools.IsToolEnabled("read_file") {
|
2026-03-09 08:32:21 +00:00
|
|
|
maxReadFileSize := cfg.Tools.ReadFile.MaxReadFileSize
|
2026-04-02 10:49:08 +00:00
|
|
|
switch cfg.Tools.ReadFile.EffectiveMode() {
|
|
|
|
|
case config.ReadFileModeLines:
|
|
|
|
|
toolsRegistry.Register(tools.NewReadFileLinesTool(workspace, readRestrict, maxReadFileSize, allowReadPaths))
|
|
|
|
|
default:
|
|
|
|
|
toolsRegistry.Register(tools.NewReadFileBytesTool(workspace, readRestrict, maxReadFileSize, allowReadPaths))
|
|
|
|
|
}
|
2026-03-05 06:53:26 +00:00
|
|
|
}
|
|
|
|
|
if cfg.Tools.IsToolEnabled("write_file") {
|
|
|
|
|
toolsRegistry.Register(tools.NewWriteFileTool(workspace, restrict, allowWritePaths))
|
|
|
|
|
}
|
|
|
|
|
if cfg.Tools.IsToolEnabled("list_dir") {
|
|
|
|
|
toolsRegistry.Register(tools.NewListDirTool(workspace, readRestrict, allowReadPaths))
|
|
|
|
|
}
|
|
|
|
|
if cfg.Tools.IsToolEnabled("exec") {
|
2026-03-14 04:02:06 +00:00
|
|
|
execTool, err := tools.NewExecToolWithConfig(workspace, restrict, cfg, allowReadPaths)
|
2026-03-05 06:53:26 +00:00
|
|
|
if err != nil {
|
2026-03-19 13:11:36 +00:00
|
|
|
logger.ErrorCF("agent", "Failed to initialize exec tool; continuing without exec",
|
|
|
|
|
map[string]any{"error": err.Error()})
|
|
|
|
|
} else {
|
|
|
|
|
toolsRegistry.Register(execTool)
|
2026-03-05 06:53:26 +00:00
|
|
|
}
|
2026-02-28 08:24:26 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 06:53:26 +00:00
|
|
|
if cfg.Tools.IsToolEnabled("edit_file") {
|
|
|
|
|
toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict, allowWritePaths))
|
|
|
|
|
}
|
|
|
|
|
if cfg.Tools.IsToolEnabled("append_file") {
|
|
|
|
|
toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict, allowWritePaths))
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
sessionsDir := filepath.Join(workspace, "sessions")
|
feat(session): integrate JSONL persistence into agent loop (#1170)
* feat(session): add SessionStore interface and JSONL backend adapter
Extract a SessionStore interface from the methods the agent loop uses
(AddMessage, GetHistory, SetSummary, TruncateHistory, Save, etc.).
Both SessionManager and the new JSONLBackend satisfy this interface,
allowing the persistence layer to be swapped transparently.
JSONLBackend wraps memory.Store and maps its error-returning API to
the fire-and-forget contract that the agent loop expects — write
errors are logged, reads return empty defaults on failure. Save()
triggers compaction to reclaim space after logical truncation.
Part of #1169
* test(session): add JSONLBackend integration tests
8 tests covering the full SessionStore contract through the JSONL
backend: message roundtrip, tool calls, summary, truncation with
compaction, history replacement, empty sessions, session isolation,
and the complete summarization flow (SetSummary → TruncateHistory →
Save).
Includes compile-time interface satisfaction checks for both
SessionManager and JSONLBackend.
Part of #1169
* feat(agent): wire JSONL session store into agent loop
Replace the concrete *SessionManager field with the SessionStore
interface and initialize the JSONL backend by default. Legacy .json
session files are auto-migrated on first startup. Falls back to
SessionManager if the JSONL store cannot be initialized.
The agent loop code (loop.go) requires zero changes — all method
calls work identically through the interface.
Closes #1169
* fix(session): propagate compact error from Save
Save() was swallowing the error returned by Compact and always
returning nil. Callers checking Save's return value would never
see a compaction failure. Return the error directly so the agent
loop can log or handle it as needed.
* feat(session): add Close to SessionStore interface
Add Close() error to SessionStore so callers can release resources
through the interface. JSONLBackend already had Close; this adds
a no-op implementation to SessionManager for compatibility.
* fix(session): close session stores on shutdown and harden migration
- Add Close() to AgentInstance, AgentRegistry, and AgentLoop so JSONL
file handles are released during gateway shutdown and CLI exit.
- Fall back to SessionManager when migration fails, preventing a split
state where some sessions live in JSONL and others remain in JSON.
- Add defer agentLoop.Close() in the CLI agent command path.
- Document SessionStore interface methods (fire-and-forget contract).
2026-03-10 07:14:09 +00:00
|
|
|
sessions := initSessionStore(sessionsDir)
|
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-03-09 17:21:49 +00:00
|
|
|
mcpDiscoveryActive := cfg.Tools.MCP.Enabled && cfg.Tools.MCP.Discovery.Enabled
|
2026-03-25 17:33:49 +00:00
|
|
|
contextBuilder := NewContextBuilder(workspace).
|
|
|
|
|
WithToolDiscovery(
|
|
|
|
|
mcpDiscoveryActive && cfg.Tools.MCP.Discovery.UseBM25,
|
|
|
|
|
mcpDiscoveryActive && cfg.Tools.MCP.Discovery.UseRegex,
|
|
|
|
|
).
|
|
|
|
|
WithSplitOnMarker(cfg.Agents.Defaults.SplitOnMarker)
|
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
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 11:21:58 +00:00
|
|
|
contextWindow := defaults.ContextWindow
|
|
|
|
|
if contextWindow == 0 {
|
|
|
|
|
// Default heuristic: 4x the output token limit.
|
|
|
|
|
// Most models have context windows well above their output limits
|
|
|
|
|
// (e.g., GPT-4o 128k ctx / 16k out, Claude 200k ctx / 8k out).
|
|
|
|
|
// 4x is a conservative lower bound that avoids premature
|
|
|
|
|
// summarization while remaining safe — the reactive
|
|
|
|
|
// forceCompression handles any overshoot.
|
|
|
|
|
contextWindow = maxTokens * 4
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 18:16:37 +00:00
|
|
|
temperature := 0.7
|
|
|
|
|
if defaults.Temperature != nil {
|
|
|
|
|
temperature = *defaults.Temperature
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 01:51:18 +00:00
|
|
|
var thinkingLevelStr string
|
|
|
|
|
if mc, err := cfg.GetModelConfig(model); err == nil {
|
|
|
|
|
thinkingLevelStr = mc.ThinkingLevel
|
|
|
|
|
}
|
|
|
|
|
thinkingLevel := parseThinkingLevel(thinkingLevelStr)
|
|
|
|
|
|
2026-03-04 03:23:01 +00:00
|
|
|
summarizeMessageThreshold := defaults.SummarizeMessageThreshold
|
|
|
|
|
if summarizeMessageThreshold == 0 {
|
|
|
|
|
summarizeMessageThreshold = 20
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
summarizeTokenPercent := defaults.SummarizeTokenPercent
|
|
|
|
|
if summarizeTokenPercent == 0 {
|
|
|
|
|
summarizeTokenPercent = 75
|
|
|
|
|
}
|
|
|
|
|
|
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
|
2026-03-19 13:44:01 +00:00
|
|
|
candidates := resolveModelCandidates(cfg, defaults.Provider, model, fallbacks)
|
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-04-07 12:07:56 +00:00
|
|
|
candidateProviders := make(map[string]providers.LLMProvider)
|
|
|
|
|
populateCandidateProvidersFromNames(cfg, workspace, fallbacks, candidateProviders)
|
|
|
|
|
|
2026-03-02 14:42:52 +00:00
|
|
|
// Model routing setup: pre-resolve light model candidates at creation time
|
|
|
|
|
// to avoid repeated model_list lookups on every incoming message.
|
|
|
|
|
var router *routing.Router
|
|
|
|
|
var lightCandidates []providers.FallbackCandidate
|
2026-03-28 07:25:23 +00:00
|
|
|
var lightProvider providers.LLMProvider
|
2026-03-02 14:42:52 +00:00
|
|
|
if rc := defaults.Routing; rc != nil && rc.Enabled && rc.LightModel != "" {
|
2026-03-19 13:44:01 +00:00
|
|
|
resolved := resolveModelCandidates(cfg, defaults.Provider, rc.LightModel, nil)
|
2026-03-02 14:42:52 +00:00
|
|
|
if len(resolved) > 0 {
|
2026-03-28 07:25:23 +00:00
|
|
|
lightModelCfg, err := resolvedModelConfig(cfg, rc.LightModel, workspace)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.WarnCF("agent", "Routing light model config invalid; routing disabled",
|
|
|
|
|
map[string]any{"light_model": rc.LightModel, "agent_id": agentID, "error": err.Error()})
|
|
|
|
|
} else {
|
|
|
|
|
lp, _, err := providers.CreateProviderFromConfig(lightModelCfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.WarnCF("agent", "Routing light model provider init failed; routing disabled",
|
|
|
|
|
map[string]any{"light_model": rc.LightModel, "agent_id": agentID, "error": err.Error()})
|
|
|
|
|
} else {
|
|
|
|
|
router = routing.New(routing.RouterConfig{
|
|
|
|
|
LightModel: rc.LightModel,
|
|
|
|
|
Threshold: rc.Threshold,
|
|
|
|
|
})
|
|
|
|
|
lightCandidates = resolved
|
|
|
|
|
lightProvider = lp
|
2026-04-07 12:07:56 +00:00
|
|
|
populateCandidateProvidersFromNames(cfg, workspace, []string{rc.LightModel}, candidateProviders)
|
2026-03-28 07:25:23 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-02 14:42:52 +00:00
|
|
|
} else {
|
2026-03-19 13:11:36 +00:00
|
|
|
logger.WarnCF("agent", "Routing light model not found; routing disabled",
|
|
|
|
|
map[string]any{"light_model": rc.LightModel, "agent_id": agentID})
|
2026-03-02 14:42:52 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
return &AgentInstance{
|
2026-03-04 03:23:01 +00:00
|
|
|
ID: agentID,
|
|
|
|
|
Name: agentName,
|
|
|
|
|
Model: model,
|
|
|
|
|
Fallbacks: fallbacks,
|
|
|
|
|
Workspace: workspace,
|
|
|
|
|
MaxIterations: maxIter,
|
|
|
|
|
MaxTokens: maxTokens,
|
|
|
|
|
Temperature: temperature,
|
2026-03-05 01:51:18 +00:00
|
|
|
ThinkingLevel: thinkingLevel,
|
2026-03-22 11:21:58 +00:00
|
|
|
ContextWindow: contextWindow,
|
2026-03-04 03:23:01 +00:00
|
|
|
SummarizeMessageThreshold: summarizeMessageThreshold,
|
|
|
|
|
SummarizeTokenPercent: summarizeTokenPercent,
|
|
|
|
|
Provider: provider,
|
feat(session): integrate JSONL persistence into agent loop (#1170)
* feat(session): add SessionStore interface and JSONL backend adapter
Extract a SessionStore interface from the methods the agent loop uses
(AddMessage, GetHistory, SetSummary, TruncateHistory, Save, etc.).
Both SessionManager and the new JSONLBackend satisfy this interface,
allowing the persistence layer to be swapped transparently.
JSONLBackend wraps memory.Store and maps its error-returning API to
the fire-and-forget contract that the agent loop expects — write
errors are logged, reads return empty defaults on failure. Save()
triggers compaction to reclaim space after logical truncation.
Part of #1169
* test(session): add JSONLBackend integration tests
8 tests covering the full SessionStore contract through the JSONL
backend: message roundtrip, tool calls, summary, truncation with
compaction, history replacement, empty sessions, session isolation,
and the complete summarization flow (SetSummary → TruncateHistory →
Save).
Includes compile-time interface satisfaction checks for both
SessionManager and JSONLBackend.
Part of #1169
* feat(agent): wire JSONL session store into agent loop
Replace the concrete *SessionManager field with the SessionStore
interface and initialize the JSONL backend by default. Legacy .json
session files are auto-migrated on first startup. Falls back to
SessionManager if the JSONL store cannot be initialized.
The agent loop code (loop.go) requires zero changes — all method
calls work identically through the interface.
Closes #1169
* fix(session): propagate compact error from Save
Save() was swallowing the error returned by Compact and always
returning nil. Callers checking Save's return value would never
see a compaction failure. Return the error directly so the agent
loop can log or handle it as needed.
* feat(session): add Close to SessionStore interface
Add Close() error to SessionStore so callers can release resources
through the interface. JSONLBackend already had Close; this adds
a no-op implementation to SessionManager for compatibility.
* fix(session): close session stores on shutdown and harden migration
- Add Close() to AgentInstance, AgentRegistry, and AgentLoop so JSONL
file handles are released during gateway shutdown and CLI exit.
- Fall back to SessionManager when migration fails, preventing a split
state where some sessions live in JSONL and others remain in JSON.
- Add defer agentLoop.Close() in the CLI agent command path.
- Document SessionStore interface methods (fire-and-forget contract).
2026-03-10 07:14:09 +00:00
|
|
|
Sessions: sessions,
|
2026-03-04 03:23:01 +00:00
|
|
|
ContextBuilder: contextBuilder,
|
|
|
|
|
Tools: toolsRegistry,
|
|
|
|
|
Subagents: subagents,
|
|
|
|
|
SkillsFilter: skillsFilter,
|
|
|
|
|
Candidates: candidates,
|
2026-03-06 03:27:48 +00:00
|
|
|
Router: router,
|
|
|
|
|
LightCandidates: lightCandidates,
|
2026-03-28 07:25:23 +00:00
|
|
|
LightProvider: lightProvider,
|
2026-04-07 12:07:56 +00:00
|
|
|
CandidateProviders: candidateProviders,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// populateCandidateProvidersFromNames resolves each model name (alias or
|
|
|
|
|
// "provider/model") via resolvedModelConfig and creates a dedicated LLMProvider
|
|
|
|
|
// for it. This reuses the canonical config resolution path (GetModelConfig) so
|
|
|
|
|
// alias handling and load-balancing stay consistent with the rest of the codebase.
|
|
|
|
|
func populateCandidateProvidersFromNames(
|
|
|
|
|
cfg *config.Config,
|
|
|
|
|
workspace string,
|
|
|
|
|
names []string,
|
|
|
|
|
out map[string]providers.LLMProvider,
|
|
|
|
|
) {
|
|
|
|
|
if cfg == nil || len(names) == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for _, name := range names {
|
|
|
|
|
mc, err := resolvedModelConfig(cfg, strings.TrimSpace(name), workspace)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.WarnCF("agent",
|
|
|
|
|
"fallback provider: no model_list entry found; will inherit primary provider credentials",
|
|
|
|
|
map[string]any{"name": name, "error": err.Error()})
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
protocol, modelID := providers.ExtractProtocol(strings.TrimSpace(mc.Model))
|
|
|
|
|
key := providers.ModelKey(providers.NormalizeProvider(protocol), modelID)
|
|
|
|
|
if _, exists := out[key]; exists {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
p, _, err := providers.CreateProviderFromConfig(mc)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.WarnCF("agent", "fallback provider: failed to create provider",
|
|
|
|
|
map[string]any{"model": mc.Model, "error": err.Error()})
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
out[key] = p
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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))
|
|
|
|
|
}
|
2026-03-05 22:08:37 +00:00
|
|
|
// Use the configured default workspace (respects PICOCLAW_HOME)
|
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 agentCfg == nil || agentCfg.Default || agentCfg.ID == "" || routing.NormalizeAgentID(agentCfg.ID) == "main" {
|
|
|
|
|
return expandHome(defaults.Workspace)
|
|
|
|
|
}
|
2026-03-05 22:08:37 +00:00
|
|
|
// For named agents without explicit workspace, use default workspace with agent ID suffix
|
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
|
|
|
id := routing.NormalizeAgentID(agentCfg.ID)
|
2026-03-05 22:08:37 +00:00
|
|
|
return filepath.Join(expandHome(defaults.Workspace), "..", "workspace-"+id)
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
fix(tools): allow /dev/null redirection and add read/write sandbox split (#967)
* fix(tools): allow /dev/null redirection and add read/write sandbox split
- Remove deny pattern that incorrectly blocked redirects to /dev/null
- Expand block device write pattern to cover nvme, mmcblk, vd, xvd,
hd, loop, dm-, md, sr and nbd in addition to sd
- Add safe path whitelist for kernel pseudo-devices so workspace path
check does not reject /dev/null, /dev/zero, /dev/random, /dev/urandom,
/dev/stdin, /dev/stdout and /dev/stderr
- Add allow_read_outside_workspace config option (default true) so file
read and list tools are unrestricted while write tools stay sandboxed
Closes https://github.com/sipeed/picoclaw/issues/964
Closes https://github.com/sipeed/picoclaw/issues/965
Signed-off-by: Huang Rui <vowstar@gmail.com>
* feat(tools): add configurable allow patterns and path whitelists
- Add custom_allow_patterns to exec config so users can exempt specific
commands from deny pattern checks
- Add allow_read_paths and allow_write_paths regex lists to tools config
for whitelisting specific paths outside the workspace
- Introduce whitelistFs that wraps sandboxFs and falls through to hostFs
for paths matching whitelist patterns
- Use variadic constructor signatures to keep backward compatibility
Suggested-by: lxowalle
Signed-off-by: Huang Rui <vowstar@gmail.com>
---------
Signed-off-by: Huang Rui <vowstar@gmail.com>
2026-03-02 04:22:02 +00:00
|
|
|
func compilePatterns(patterns []string) []*regexp.Regexp {
|
|
|
|
|
compiled := make([]*regexp.Regexp, 0, len(patterns))
|
|
|
|
|
for _, p := range patterns {
|
|
|
|
|
re, err := regexp.Compile(p)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf("Warning: invalid path pattern %q: %v\n", p, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
compiled = append(compiled, re)
|
|
|
|
|
}
|
|
|
|
|
return compiled
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 04:02:06 +00:00
|
|
|
func buildAllowReadPatterns(cfg *config.Config) []*regexp.Regexp {
|
|
|
|
|
var configured []string
|
|
|
|
|
if cfg != nil {
|
|
|
|
|
configured = cfg.Tools.AllowReadPaths
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
compiled := compilePatterns(configured)
|
|
|
|
|
mediaDirPattern := regexp.MustCompile(mediaTempDirPattern())
|
|
|
|
|
for _, pattern := range compiled {
|
|
|
|
|
if pattern.String() == mediaDirPattern.String() {
|
|
|
|
|
return compiled
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return append(compiled, mediaDirPattern)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func mediaTempDirPattern() string {
|
|
|
|
|
sep := regexp.QuoteMeta(string(os.PathSeparator))
|
|
|
|
|
return "^" + regexp.QuoteMeta(filepath.Clean(media.TempDir())) + "(?:" + sep + "|$)"
|
|
|
|
|
}
|
|
|
|
|
|
feat(session): integrate JSONL persistence into agent loop (#1170)
* feat(session): add SessionStore interface and JSONL backend adapter
Extract a SessionStore interface from the methods the agent loop uses
(AddMessage, GetHistory, SetSummary, TruncateHistory, Save, etc.).
Both SessionManager and the new JSONLBackend satisfy this interface,
allowing the persistence layer to be swapped transparently.
JSONLBackend wraps memory.Store and maps its error-returning API to
the fire-and-forget contract that the agent loop expects — write
errors are logged, reads return empty defaults on failure. Save()
triggers compaction to reclaim space after logical truncation.
Part of #1169
* test(session): add JSONLBackend integration tests
8 tests covering the full SessionStore contract through the JSONL
backend: message roundtrip, tool calls, summary, truncation with
compaction, history replacement, empty sessions, session isolation,
and the complete summarization flow (SetSummary → TruncateHistory →
Save).
Includes compile-time interface satisfaction checks for both
SessionManager and JSONLBackend.
Part of #1169
* feat(agent): wire JSONL session store into agent loop
Replace the concrete *SessionManager field with the SessionStore
interface and initialize the JSONL backend by default. Legacy .json
session files are auto-migrated on first startup. Falls back to
SessionManager if the JSONL store cannot be initialized.
The agent loop code (loop.go) requires zero changes — all method
calls work identically through the interface.
Closes #1169
* fix(session): propagate compact error from Save
Save() was swallowing the error returned by Compact and always
returning nil. Callers checking Save's return value would never
see a compaction failure. Return the error directly so the agent
loop can log or handle it as needed.
* feat(session): add Close to SessionStore interface
Add Close() error to SessionStore so callers can release resources
through the interface. JSONLBackend already had Close; this adds
a no-op implementation to SessionManager for compatibility.
* fix(session): close session stores on shutdown and harden migration
- Add Close() to AgentInstance, AgentRegistry, and AgentLoop so JSONL
file handles are released during gateway shutdown and CLI exit.
- Fall back to SessionManager when migration fails, preventing a split
state where some sessions live in JSONL and others remain in JSON.
- Add defer agentLoop.Close() in the CLI agent command path.
- Document SessionStore interface methods (fire-and-forget contract).
2026-03-10 07:14:09 +00:00
|
|
|
// Close releases resources held by the agent's session store.
|
|
|
|
|
func (a *AgentInstance) Close() error {
|
|
|
|
|
if a.Sessions != nil {
|
|
|
|
|
return a.Sessions.Close()
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// initSessionStore creates the session persistence backend.
|
|
|
|
|
// It uses the JSONL store by default and auto-migrates legacy JSON sessions.
|
|
|
|
|
// Falls back to SessionManager if the JSONL store cannot be initialized or
|
|
|
|
|
// if migration fails (which indicates the store cannot write reliably).
|
|
|
|
|
func initSessionStore(dir string) session.SessionStore {
|
|
|
|
|
store, err := memory.NewJSONLStore(dir)
|
|
|
|
|
if err != nil {
|
2026-03-19 13:11:36 +00:00
|
|
|
logger.WarnCF("agent", "Memory JSONL store init failed; falling back to json sessions",
|
|
|
|
|
map[string]any{"error": err.Error()})
|
feat(session): integrate JSONL persistence into agent loop (#1170)
* feat(session): add SessionStore interface and JSONL backend adapter
Extract a SessionStore interface from the methods the agent loop uses
(AddMessage, GetHistory, SetSummary, TruncateHistory, Save, etc.).
Both SessionManager and the new JSONLBackend satisfy this interface,
allowing the persistence layer to be swapped transparently.
JSONLBackend wraps memory.Store and maps its error-returning API to
the fire-and-forget contract that the agent loop expects — write
errors are logged, reads return empty defaults on failure. Save()
triggers compaction to reclaim space after logical truncation.
Part of #1169
* test(session): add JSONLBackend integration tests
8 tests covering the full SessionStore contract through the JSONL
backend: message roundtrip, tool calls, summary, truncation with
compaction, history replacement, empty sessions, session isolation,
and the complete summarization flow (SetSummary → TruncateHistory →
Save).
Includes compile-time interface satisfaction checks for both
SessionManager and JSONLBackend.
Part of #1169
* feat(agent): wire JSONL session store into agent loop
Replace the concrete *SessionManager field with the SessionStore
interface and initialize the JSONL backend by default. Legacy .json
session files are auto-migrated on first startup. Falls back to
SessionManager if the JSONL store cannot be initialized.
The agent loop code (loop.go) requires zero changes — all method
calls work identically through the interface.
Closes #1169
* fix(session): propagate compact error from Save
Save() was swallowing the error returned by Compact and always
returning nil. Callers checking Save's return value would never
see a compaction failure. Return the error directly so the agent
loop can log or handle it as needed.
* feat(session): add Close to SessionStore interface
Add Close() error to SessionStore so callers can release resources
through the interface. JSONLBackend already had Close; this adds
a no-op implementation to SessionManager for compatibility.
* fix(session): close session stores on shutdown and harden migration
- Add Close() to AgentInstance, AgentRegistry, and AgentLoop so JSONL
file handles are released during gateway shutdown and CLI exit.
- Fall back to SessionManager when migration fails, preventing a split
state where some sessions live in JSONL and others remain in JSON.
- Add defer agentLoop.Close() in the CLI agent command path.
- Document SessionStore interface methods (fire-and-forget contract).
2026-03-10 07:14:09 +00:00
|
|
|
return session.NewSessionManager(dir)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if n, merr := memory.MigrateFromJSON(context.Background(), dir, store); merr != nil {
|
|
|
|
|
// Migration failure means the store could not write data.
|
|
|
|
|
// Fall back to SessionManager to avoid a split state where
|
|
|
|
|
// some sessions are in JSONL and others remain in JSON.
|
2026-03-19 13:11:36 +00:00
|
|
|
logger.WarnCF("agent", "Memory migration failed; falling back to json sessions",
|
|
|
|
|
map[string]any{"error": merr.Error()})
|
feat(session): integrate JSONL persistence into agent loop (#1170)
* feat(session): add SessionStore interface and JSONL backend adapter
Extract a SessionStore interface from the methods the agent loop uses
(AddMessage, GetHistory, SetSummary, TruncateHistory, Save, etc.).
Both SessionManager and the new JSONLBackend satisfy this interface,
allowing the persistence layer to be swapped transparently.
JSONLBackend wraps memory.Store and maps its error-returning API to
the fire-and-forget contract that the agent loop expects — write
errors are logged, reads return empty defaults on failure. Save()
triggers compaction to reclaim space after logical truncation.
Part of #1169
* test(session): add JSONLBackend integration tests
8 tests covering the full SessionStore contract through the JSONL
backend: message roundtrip, tool calls, summary, truncation with
compaction, history replacement, empty sessions, session isolation,
and the complete summarization flow (SetSummary → TruncateHistory →
Save).
Includes compile-time interface satisfaction checks for both
SessionManager and JSONLBackend.
Part of #1169
* feat(agent): wire JSONL session store into agent loop
Replace the concrete *SessionManager field with the SessionStore
interface and initialize the JSONL backend by default. Legacy .json
session files are auto-migrated on first startup. Falls back to
SessionManager if the JSONL store cannot be initialized.
The agent loop code (loop.go) requires zero changes — all method
calls work identically through the interface.
Closes #1169
* fix(session): propagate compact error from Save
Save() was swallowing the error returned by Compact and always
returning nil. Callers checking Save's return value would never
see a compaction failure. Return the error directly so the agent
loop can log or handle it as needed.
* feat(session): add Close to SessionStore interface
Add Close() error to SessionStore so callers can release resources
through the interface. JSONLBackend already had Close; this adds
a no-op implementation to SessionManager for compatibility.
* fix(session): close session stores on shutdown and harden migration
- Add Close() to AgentInstance, AgentRegistry, and AgentLoop so JSONL
file handles are released during gateway shutdown and CLI exit.
- Fall back to SessionManager when migration fails, preventing a split
state where some sessions live in JSONL and others remain in JSON.
- Add defer agentLoop.Close() in the CLI agent command path.
- Document SessionStore interface methods (fire-and-forget contract).
2026-03-10 07:14:09 +00:00
|
|
|
store.Close()
|
|
|
|
|
return session.NewSessionManager(dir)
|
|
|
|
|
} else if n > 0 {
|
2026-03-19 13:11:36 +00:00
|
|
|
logger.InfoCF("agent", "Memory migrated to JSONL", map[string]any{"sessions_migrated": n})
|
feat(session): integrate JSONL persistence into agent loop (#1170)
* feat(session): add SessionStore interface and JSONL backend adapter
Extract a SessionStore interface from the methods the agent loop uses
(AddMessage, GetHistory, SetSummary, TruncateHistory, Save, etc.).
Both SessionManager and the new JSONLBackend satisfy this interface,
allowing the persistence layer to be swapped transparently.
JSONLBackend wraps memory.Store and maps its error-returning API to
the fire-and-forget contract that the agent loop expects — write
errors are logged, reads return empty defaults on failure. Save()
triggers compaction to reclaim space after logical truncation.
Part of #1169
* test(session): add JSONLBackend integration tests
8 tests covering the full SessionStore contract through the JSONL
backend: message roundtrip, tool calls, summary, truncation with
compaction, history replacement, empty sessions, session isolation,
and the complete summarization flow (SetSummary → TruncateHistory →
Save).
Includes compile-time interface satisfaction checks for both
SessionManager and JSONLBackend.
Part of #1169
* feat(agent): wire JSONL session store into agent loop
Replace the concrete *SessionManager field with the SessionStore
interface and initialize the JSONL backend by default. Legacy .json
session files are auto-migrated on first startup. Falls back to
SessionManager if the JSONL store cannot be initialized.
The agent loop code (loop.go) requires zero changes — all method
calls work identically through the interface.
Closes #1169
* fix(session): propagate compact error from Save
Save() was swallowing the error returned by Compact and always
returning nil. Callers checking Save's return value would never
see a compaction failure. Return the error directly so the agent
loop can log or handle it as needed.
* feat(session): add Close to SessionStore interface
Add Close() error to SessionStore so callers can release resources
through the interface. JSONLBackend already had Close; this adds
a no-op implementation to SessionManager for compatibility.
* fix(session): close session stores on shutdown and harden migration
- Add Close() to AgentInstance, AgentRegistry, and AgentLoop so JSONL
file handles are released during gateway shutdown and CLI exit.
- Fall back to SessionManager when migration fails, preventing a split
state where some sessions live in JSONL and others remain in JSON.
- Add defer agentLoop.Close() in the CLI agent command path.
- Document SessionStore interface methods (fire-and-forget contract).
2026-03-10 07:14:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return session.NewJSONLBackend(store)
|
|
|
|
|
}
|
|
|
|
|
|
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 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
|
|
|
|
|
}
|