mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 18:18:37 +00:00
fix: honor per-agent DB settings for restrict, subagents, memory, sandbox (#145)
Four per-agent settings stored in the database (and configurable via UI) were silently ignored at runtime because the tool/system layer always used the global config defaults instead. **restrict_to_workspace**: Tools used the global config default baked at startup. Fix: pass per-agent value through context; tools check context override before falling back to constructor default. **subagents_config**: ParseSubagentsConfig() existed but was never called. All agents shared one SubagentManager with global limits. Fix: resolve per-agent config in the agent resolver, store it on each spawned task, and use it for limit checks, deny lists, and system prompt generation. **memory_config**: Only the enabled toggle was read per-agent; search weights (vector_weight, text_weight, max_results, min_score) were hardcoded from PGMemoryStore defaults. Fix: extend MemorySearchOptions with weight overrides, read per-agent config from context in the memory_search tool. **sandbox_config**: Only workspace_access was extracted per-agent; mode, image, memory, CPU, timeout, network settings were discarded. Fix: pass full sandbox.Config through context; Manager.Get() accepts an optional config override for new containers. Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
This commit is contained in:
1 parent
1b99406012
commit
2fdb791802
17 files changed
+204
-41
No files matched your search
@@ -73,6 +73,19 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
if req.ChannelType != "" {
|
||||
ctx = tools.WithToolChannelType(ctx, req.ChannelType)
|
||||
}
|
||||
// Inject per-agent overrides from DB so tools honor per-agent settings.
|
||||
if l.restrictToWs != nil {
|
||||
ctx = tools.WithRestrictToWorkspace(ctx, *l.restrictToWs)
|
||||
}
|
||||
if l.subagentsCfg != nil {
|
||||
ctx = tools.WithSubagentConfig(ctx, l.subagentsCfg)
|
||||
}
|
||||
if l.memoryCfg != nil {
|
||||
ctx = tools.WithMemoryConfig(ctx, l.memoryCfg)
|
||||
}
|
||||
if l.sandboxCfg != nil {
|
||||
ctx = tools.WithSandboxConfig(ctx, l.sandboxCfg)
|
||||
}
|
||||
|
||||
// Per-user workspace isolation.
|
||||
// Workspace path comes from user_agent_profiles (includes channel segment
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/media"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/sandbox"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/skills"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
||||
@@ -47,6 +48,12 @@ type Loop struct {
|
||||
maxToolCalls int
|
||||
workspace string
|
||||
|
||||
// Per-agent overrides from DB (nil = use global defaults)
|
||||
restrictToWs *bool
|
||||
subagentsCfg *config.SubagentsConfig
|
||||
memoryCfg *config.MemoryConfig
|
||||
sandboxCfg *sandbox.Config
|
||||
|
||||
eventPub bus.EventPublisher // currently unused by Loop; kept for future use
|
||||
sessions store.SessionStore
|
||||
tools *tools.Registry
|
||||
@@ -147,6 +154,13 @@ type LoopConfig struct {
|
||||
MaxIterations int
|
||||
MaxToolCalls int
|
||||
Workspace string
|
||||
|
||||
// Per-agent DB overrides (nil = use global defaults)
|
||||
RestrictToWs *bool
|
||||
SubagentsCfg *config.SubagentsConfig
|
||||
MemoryCfg *config.MemoryConfig
|
||||
SandboxCfg *sandbox.Config
|
||||
|
||||
Bus bus.EventPublisher
|
||||
Sessions store.SessionStore
|
||||
Tools *tools.Registry
|
||||
@@ -248,6 +262,10 @@ func NewLoop(cfg LoopConfig) *Loop {
|
||||
maxIterations: cfg.MaxIterations,
|
||||
maxToolCalls: cfg.MaxToolCalls,
|
||||
workspace: cfg.Workspace,
|
||||
restrictToWs: cfg.RestrictToWs,
|
||||
subagentsCfg: cfg.SubagentsCfg,
|
||||
memoryCfg: cfg.MemoryCfg,
|
||||
sandboxCfg: cfg.SandboxCfg,
|
||||
eventPub: cfg.Bus,
|
||||
sessions: cfg.Sessions,
|
||||
tools: cfg.Tools,
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
mcpbridge "github.com/nextlevelbuilder/goclaw/internal/mcp"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/media"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/sandbox"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/skills"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
||||
@@ -243,10 +244,12 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
|
||||
sandboxEnabled := deps.SandboxEnabled
|
||||
sandboxContainerDir := deps.SandboxContainerDir
|
||||
sandboxWorkspaceAccess := deps.SandboxWorkspaceAccess
|
||||
var sandboxCfgOverride *sandbox.Config
|
||||
if c := ag.ParseSandboxConfig(); c != nil {
|
||||
resolved := c.ToSandboxConfig()
|
||||
sandboxContainerDir = resolved.ContainerWorkdir()
|
||||
sandboxWorkspaceAccess = string(resolved.WorkspaceAccess)
|
||||
sandboxCfgOverride = &resolved
|
||||
}
|
||||
|
||||
// Expand ~ in workspace path and ensure directory exists
|
||||
@@ -345,6 +348,7 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
|
||||
}
|
||||
}
|
||||
|
||||
restrictVal := ag.RestrictToWorkspace
|
||||
loop := NewLoop(LoopConfig{
|
||||
ID: ag.AgentKey,
|
||||
AgentUUID: ag.ID,
|
||||
@@ -354,6 +358,10 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
|
||||
ContextWindow: contextWindow,
|
||||
MaxIterations: maxIter,
|
||||
Workspace: workspace,
|
||||
RestrictToWs: &restrictVal,
|
||||
SubagentsCfg: ag.ParseSubagentsConfig(),
|
||||
MemoryCfg: ag.ParseMemoryConfig(),
|
||||
SandboxCfg: sandboxCfgOverride,
|
||||
Bus: deps.Bus,
|
||||
Sessions: deps.Sessions,
|
||||
Tools: toolsReg,
|
||||
|
||||
@@ -230,8 +230,13 @@ func NewDockerManager(cfg Config) *DockerManager {
|
||||
}
|
||||
|
||||
// Get returns an existing sandbox or creates a new one for the given key.
|
||||
func (m *DockerManager) Get(ctx context.Context, key string, workspace string) (Sandbox, error) {
|
||||
if m.config.Mode == ModeOff {
|
||||
// If cfgOverride is non-nil, it is used for new containers instead of the global config.
|
||||
func (m *DockerManager) Get(ctx context.Context, key string, workspace string, cfgOverride *Config) (Sandbox, error) {
|
||||
cfg := m.config
|
||||
if cfgOverride != nil {
|
||||
cfg = *cfgOverride
|
||||
}
|
||||
if cfg.Mode == ModeOff {
|
||||
return nil, ErrSandboxDisabled
|
||||
}
|
||||
|
||||
@@ -250,12 +255,12 @@ func (m *DockerManager) Get(ctx context.Context, key string, workspace string) (
|
||||
return sb, nil
|
||||
}
|
||||
|
||||
prefix := m.config.ContainerPrefix
|
||||
prefix := cfg.ContainerPrefix
|
||||
if prefix == "" {
|
||||
prefix = "goclaw-sbx-"
|
||||
}
|
||||
name := prefix + sanitizeKey(key)
|
||||
sb, err := newDockerSandbox(ctx, name, m.config, workspace)
|
||||
sb, err := newDockerSandbox(ctx, name, cfg, workspace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -171,7 +171,8 @@ type Manager interface {
|
||||
// For session scope: key = sessionKey
|
||||
// For agent scope: key = agentID
|
||||
// For shared scope: key = "shared"
|
||||
Get(ctx context.Context, key string, workspace string) (Sandbox, error)
|
||||
// If cfgOverride is non-nil, it is used instead of the global config for new containers.
|
||||
Get(ctx context.Context, key string, workspace string, cfgOverride *Config) (Sandbox, error)
|
||||
|
||||
// Release destroys a sandbox by key.
|
||||
Release(ctx context.Context, key string) error
|
||||
|
||||
@@ -24,10 +24,12 @@ type MemorySearchResult struct {
|
||||
|
||||
// MemorySearchOptions configures a memory search query.
|
||||
type MemorySearchOptions struct {
|
||||
MaxResults int
|
||||
MinScore float64
|
||||
Source string // "memory", "sessions", ""
|
||||
PathPrefix string
|
||||
MaxResults int
|
||||
MinScore float64
|
||||
Source string // "memory", "sessions", ""
|
||||
PathPrefix string
|
||||
VectorWeight float64 // per-agent override (0 = use store default)
|
||||
TextWeight float64 // per-agent override (0 = use store default)
|
||||
}
|
||||
|
||||
// EmbeddingProvider generates vector embeddings for text.
|
||||
|
||||
@@ -34,8 +34,14 @@ func (s *PGMemoryStore) Search(ctx context.Context, query string, agentID, userI
|
||||
}
|
||||
}
|
||||
|
||||
// Merge results — normalize weights when one channel has no results
|
||||
// Merge results — use per-query overrides if set, else store defaults
|
||||
textW, vecW := s.cfg.TextWeight, s.cfg.VectorWeight
|
||||
if opts.TextWeight > 0 {
|
||||
textW = opts.TextWeight
|
||||
}
|
||||
if opts.VectorWeight > 0 {
|
||||
vecW = opts.VectorWeight
|
||||
}
|
||||
if len(ftsResults) == 0 && len(vecResults) > 0 {
|
||||
textW, vecW = 0, 1.0
|
||||
} else if len(vecResults) == 0 && len(ftsResults) > 0 {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/sandbox"
|
||||
)
|
||||
|
||||
// Tool execution context keys.
|
||||
@@ -164,3 +165,64 @@ func BuiltinToolSettingsFromCtx(ctx context.Context) BuiltinToolSettings {
|
||||
v, _ := ctx.Value(ctxBuiltinToolSettings).(BuiltinToolSettings)
|
||||
return v
|
||||
}
|
||||
|
||||
// --- Per-agent restrict_to_workspace override ---
|
||||
|
||||
const ctxRestrictWs toolContextKey = "tool_restrict_to_workspace"
|
||||
|
||||
// WithRestrictToWorkspace injects a per-agent restrict_to_workspace override into context.
|
||||
func WithRestrictToWorkspace(ctx context.Context, restrict bool) context.Context {
|
||||
return context.WithValue(ctx, ctxRestrictWs, restrict)
|
||||
}
|
||||
|
||||
// RestrictFromCtx returns the per-agent restrict_to_workspace override.
|
||||
func RestrictFromCtx(ctx context.Context) (bool, bool) {
|
||||
v, ok := ctx.Value(ctxRestrictWs).(bool)
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func effectiveRestrict(ctx context.Context, toolDefault bool) bool {
|
||||
if v, ok := RestrictFromCtx(ctx); ok {
|
||||
return v
|
||||
}
|
||||
return toolDefault
|
||||
}
|
||||
|
||||
// --- Per-agent subagent config override ---
|
||||
|
||||
const ctxSubagentCfg toolContextKey = "tool_subagent_config"
|
||||
|
||||
func WithSubagentConfig(ctx context.Context, cfg *config.SubagentsConfig) context.Context {
|
||||
return context.WithValue(ctx, ctxSubagentCfg, cfg)
|
||||
}
|
||||
|
||||
func SubagentConfigFromCtx(ctx context.Context) *config.SubagentsConfig {
|
||||
v, _ := ctx.Value(ctxSubagentCfg).(*config.SubagentsConfig)
|
||||
return v
|
||||
}
|
||||
|
||||
// --- Per-agent memory config override ---
|
||||
|
||||
const ctxMemoryCfg toolContextKey = "tool_memory_config"
|
||||
|
||||
func WithMemoryConfig(ctx context.Context, cfg *config.MemoryConfig) context.Context {
|
||||
return context.WithValue(ctx, ctxMemoryCfg, cfg)
|
||||
}
|
||||
|
||||
func MemoryConfigFromCtx(ctx context.Context) *config.MemoryConfig {
|
||||
v, _ := ctx.Value(ctxMemoryCfg).(*config.MemoryConfig)
|
||||
return v
|
||||
}
|
||||
|
||||
// --- Per-agent sandbox config override ---
|
||||
|
||||
const ctxSandboxCfg toolContextKey = "tool_sandbox_config"
|
||||
|
||||
func WithSandboxConfig(ctx context.Context, cfg *sandbox.Config) context.Context {
|
||||
return context.WithValue(ctx, ctxSandboxCfg, cfg)
|
||||
}
|
||||
|
||||
func SandboxConfigFromCtx(ctx context.Context) *sandbox.Config {
|
||||
v, _ := ctx.Value(ctxSandboxCfg).(*sandbox.Config)
|
||||
return v
|
||||
}
|
||||
@@ -160,7 +160,7 @@ func (t *EditTool) Execute(ctx context.Context, args map[string]any) *Result {
|
||||
if workspace == "" {
|
||||
workspace = t.workspace
|
||||
}
|
||||
resolved, err := resolvePath(path, workspace, t.restrict)
|
||||
resolved, err := resolvePath(path, workspace, effectiveRestrict(ctx, t.restrict))
|
||||
if err != nil {
|
||||
return ErrorResult(err.Error())
|
||||
}
|
||||
@@ -192,7 +192,7 @@ func (t *EditTool) Execute(ctx context.Context, args map[string]any) *Result {
|
||||
}
|
||||
|
||||
func (t *EditTool) executeInSandbox(ctx context.Context, path, oldStr, newStr string, replaceAll bool, sandboxKey string) *Result {
|
||||
sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workspace)
|
||||
sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workspace, SandboxConfigFromCtx(ctx))
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("sandbox error: %v", err))
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *Result
|
||||
if workspace == "" {
|
||||
workspace = t.workspace
|
||||
}
|
||||
resolved, err := resolvePathWithAllowed(path, workspace, t.restrict, t.allowedPrefixes)
|
||||
resolved, err := resolvePathWithAllowed(path, workspace, effectiveRestrict(ctx, t.restrict), t.allowedPrefixes)
|
||||
if err != nil {
|
||||
return ErrorResult(err.Error())
|
||||
}
|
||||
@@ -177,7 +177,7 @@ func (t *ReadFileTool) executeInSandbox(ctx context.Context, path, sandboxKey st
|
||||
}
|
||||
|
||||
func (t *ReadFileTool) getFsBridge(ctx context.Context, sandboxKey string) (*sandbox.FsBridge, error) {
|
||||
sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workspace)
|
||||
sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workspace, SandboxConfigFromCtx(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func (t *ListFilesTool) Execute(ctx context.Context, args map[string]any) *Resul
|
||||
if workspace == "" {
|
||||
workspace = t.workspace
|
||||
}
|
||||
resolved, err := resolvePath(path, workspace, t.restrict)
|
||||
resolved, err := resolvePath(path, workspace, effectiveRestrict(ctx, t.restrict))
|
||||
if err != nil {
|
||||
return ErrorResult(err.Error())
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func (t *ListFilesTool) executeInSandbox(ctx context.Context, path, sandboxKey s
|
||||
}
|
||||
|
||||
func (t *ListFilesTool) getFsBridge(ctx context.Context, sandboxKey string) (*sandbox.FsBridge, error) {
|
||||
sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workspace)
|
||||
sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workspace, SandboxConfigFromCtx(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ func (t *WriteFileTool) Execute(ctx context.Context, args map[string]any) *Resul
|
||||
if workspace == "" {
|
||||
workspace = t.workspace
|
||||
}
|
||||
resolved, err := resolvePath(path, workspace, t.restrict)
|
||||
resolved, err := resolvePath(path, workspace, effectiveRestrict(ctx, t.restrict))
|
||||
if err != nil {
|
||||
return ErrorResult(err.Error())
|
||||
}
|
||||
@@ -177,7 +177,7 @@ func (t *WriteFileTool) executeInSandbox(ctx context.Context, path, content, san
|
||||
}
|
||||
|
||||
func (t *WriteFileTool) getFsBridge(ctx context.Context, sandboxKey string) (*sandbox.FsBridge, error) {
|
||||
sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workspace)
|
||||
sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workspace, SandboxConfigFromCtx(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -73,10 +73,26 @@ func (t *MemorySearchTool) Execute(ctx context.Context, args map[string]any) *Re
|
||||
}
|
||||
|
||||
userID := store.UserIDFromContext(ctx)
|
||||
results, err := t.memStore.Search(ctx, query, agentID.String(), userID, store.MemorySearchOptions{
|
||||
searchOpts := store.MemorySearchOptions{
|
||||
MaxResults: maxResults,
|
||||
MinScore: minScore,
|
||||
})
|
||||
}
|
||||
// Apply per-agent memory config overrides if set
|
||||
if mc := MemoryConfigFromCtx(ctx); mc != nil {
|
||||
if mc.MaxResults > 0 && searchOpts.MaxResults <= 0 {
|
||||
searchOpts.MaxResults = mc.MaxResults
|
||||
}
|
||||
if mc.VectorWeight > 0 {
|
||||
searchOpts.VectorWeight = mc.VectorWeight
|
||||
}
|
||||
if mc.TextWeight > 0 {
|
||||
searchOpts.TextWeight = mc.TextWeight
|
||||
}
|
||||
if mc.MinScore > 0 && searchOpts.MinScore <= 0 {
|
||||
searchOpts.MinScore = mc.MinScore
|
||||
}
|
||||
}
|
||||
results, err := t.memStore.Search(ctx, query, agentID.String(), userID, searchOpts)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("memory search failed: %v", err))
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *Result {
|
||||
cwd = t.workingDir
|
||||
}
|
||||
if wd, _ := args["working_dir"].(string); wd != "" {
|
||||
if t.restrict {
|
||||
if effectiveRestrict(ctx, t.restrict) {
|
||||
resolved, err := resolvePath(wd, t.workingDir, true)
|
||||
if err != nil {
|
||||
return ErrorResult(err.Error())
|
||||
@@ -315,7 +315,7 @@ func (t *ExecTool) executeOnHost(ctx context.Context, command, cwd string) *Resu
|
||||
|
||||
// executeInSandbox routes a command through a Docker sandbox container.
|
||||
func (t *ExecTool) executeInSandbox(ctx context.Context, command, cwd, sandboxKey string) *Result {
|
||||
sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workingDir)
|
||||
sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workingDir, SandboxConfigFromCtx(ctx))
|
||||
if err != nil {
|
||||
if errors.Is(err, sandbox.ErrSandboxDisabled) {
|
||||
return t.executeOnHost(ctx, command, cwd)
|
||||
|
||||
@@ -63,6 +63,7 @@ type SubagentTask struct {
|
||||
OriginTraceID uuid.UUID `json:"-"` // parent trace for announce linking
|
||||
OriginRootSpanID uuid.UUID `json:"-"` // parent agent's root span ID
|
||||
cancelFunc context.CancelFunc `json:"-"` // per-task context cancel
|
||||
spawnConfig SubagentConfig `json:"-"` // resolved config at spawn time (per-agent override merged)
|
||||
}
|
||||
|
||||
// SubagentManager manages the lifecycle of spawned subagents.
|
||||
@@ -103,6 +104,32 @@ func (sm *SubagentManager) SetAnnounceQueue(q *AnnounceQueue) {
|
||||
sm.announceQueue = q
|
||||
}
|
||||
|
||||
// effectiveConfig returns the per-agent context override merged with defaults,
|
||||
// or falls back to sm.config when no override is present.
|
||||
func (sm *SubagentManager) effectiveConfig(ctx context.Context) SubagentConfig {
|
||||
override := SubagentConfigFromCtx(ctx)
|
||||
if override == nil {
|
||||
return sm.config
|
||||
}
|
||||
cfg := sm.config
|
||||
if override.MaxConcurrent > 0 {
|
||||
cfg.MaxConcurrent = override.MaxConcurrent
|
||||
}
|
||||
if override.MaxSpawnDepth > 0 {
|
||||
cfg.MaxSpawnDepth = override.MaxSpawnDepth
|
||||
}
|
||||
if override.MaxChildrenPerAgent > 0 {
|
||||
cfg.MaxChildrenPerAgent = override.MaxChildrenPerAgent
|
||||
}
|
||||
if override.ArchiveAfterMinutes > 0 {
|
||||
cfg.ArchiveAfterMinutes = override.ArchiveAfterMinutes
|
||||
}
|
||||
if override.Model != "" {
|
||||
cfg.Model = override.Model
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// CountRunningForParent returns the number of running tasks for a parent.
|
||||
func (sm *SubagentManager) CountRunningForParent(parentID string) int {
|
||||
sm.mu.RLock()
|
||||
@@ -147,12 +174,13 @@ func (sm *SubagentManager) Spawn(
|
||||
channel, chatID, peerKind string,
|
||||
callback AsyncCallback,
|
||||
) (string, error) {
|
||||
cfg := sm.effectiveConfig(ctx)
|
||||
sm.mu.Lock()
|
||||
|
||||
// Check depth limit
|
||||
if depth >= sm.config.MaxSpawnDepth {
|
||||
if depth >= cfg.MaxSpawnDepth {
|
||||
sm.mu.Unlock()
|
||||
return "", fmt.Errorf("spawn depth limit reached (%d/%d)", depth, sm.config.MaxSpawnDepth)
|
||||
return "", fmt.Errorf("spawn depth limit reached (%d/%d)", depth, cfg.MaxSpawnDepth)
|
||||
}
|
||||
|
||||
// Check concurrent limit
|
||||
@@ -162,9 +190,9 @@ func (sm *SubagentManager) Spawn(
|
||||
running++
|
||||
}
|
||||
}
|
||||
if running >= sm.config.MaxConcurrent {
|
||||
if running >= cfg.MaxConcurrent {
|
||||
sm.mu.Unlock()
|
||||
return "", fmt.Errorf("max concurrent subagents reached (%d/%d)", running, sm.config.MaxConcurrent)
|
||||
return "", fmt.Errorf("max concurrent subagents reached (%d/%d)", running, cfg.MaxConcurrent)
|
||||
}
|
||||
|
||||
// Check per-parent children limit
|
||||
@@ -174,9 +202,9 @@ func (sm *SubagentManager) Spawn(
|
||||
childCount++
|
||||
}
|
||||
}
|
||||
if childCount >= sm.config.MaxChildrenPerAgent {
|
||||
if childCount >= cfg.MaxChildrenPerAgent {
|
||||
sm.mu.Unlock()
|
||||
return "", fmt.Errorf("max children per agent reached (%d/%d)", childCount, sm.config.MaxChildrenPerAgent)
|
||||
return "", fmt.Errorf("max children per agent reached (%d/%d)", childCount, cfg.MaxChildrenPerAgent)
|
||||
}
|
||||
|
||||
id := generateSubagentID()
|
||||
@@ -201,6 +229,7 @@ func (sm *SubagentManager) Spawn(
|
||||
OriginTraceID: tracing.TraceIDFromContext(ctx),
|
||||
OriginRootSpanID: tracing.ParentSpanIDFromContext(ctx),
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
spawnConfig: cfg,
|
||||
}
|
||||
// Create per-task context for real goroutine cancellation
|
||||
taskCtx, taskCancel := context.WithCancel(ctx)
|
||||
@@ -225,11 +254,13 @@ func (sm *SubagentManager) RunSync(
|
||||
task, label string,
|
||||
channel, chatID string,
|
||||
) (string, int, error) {
|
||||
cfg := sm.effectiveConfig(ctx)
|
||||
|
||||
sm.mu.Lock()
|
||||
|
||||
if depth >= sm.config.MaxSpawnDepth {
|
||||
if depth >= cfg.MaxSpawnDepth {
|
||||
sm.mu.Unlock()
|
||||
return "", 0, fmt.Errorf("spawn depth limit reached (%d/%d)", depth, sm.config.MaxSpawnDepth)
|
||||
return "", 0, fmt.Errorf("spawn depth limit reached (%d/%d)", depth, cfg.MaxSpawnDepth)
|
||||
}
|
||||
|
||||
id := generateSubagentID()
|
||||
@@ -251,6 +282,7 @@ func (sm *SubagentManager) RunSync(
|
||||
OriginTraceID: tracing.TraceIDFromContext(ctx),
|
||||
OriginRootSpanID: tracing.ParentSpanIDFromContext(ctx),
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
spawnConfig: cfg,
|
||||
}
|
||||
sm.tasks[id] = subTask
|
||||
sm.mu.Unlock()
|
||||
|
||||
@@ -14,14 +14,14 @@ func DefaultSubagentConfig() SubagentConfig {
|
||||
}
|
||||
|
||||
// applyDenyList removes denied tools from the registry based on depth.
|
||||
func (sm *SubagentManager) applyDenyList(reg *Registry, depth int) {
|
||||
func (sm *SubagentManager) applyDenyList(reg *Registry, depth int, cfg SubagentConfig) {
|
||||
// Always deny
|
||||
for _, name := range SubagentDenyAlways {
|
||||
reg.Unregister(name)
|
||||
}
|
||||
|
||||
// Leaf deny (at max depth)
|
||||
if depth >= sm.config.MaxSpawnDepth {
|
||||
if depth >= cfg.MaxSpawnDepth {
|
||||
for _, name := range SubagentDenyLeaf {
|
||||
reg.Unregister(name)
|
||||
}
|
||||
@@ -30,13 +30,13 @@ func (sm *SubagentManager) applyDenyList(reg *Registry, depth int) {
|
||||
|
||||
// buildSubagentSystemPrompt constructs the system prompt for a subagent,
|
||||
// matching the TS buildSubagentSystemPrompt pattern from subagent-announce.ts.
|
||||
func (sm *SubagentManager) buildSubagentSystemPrompt(task *SubagentTask) string {
|
||||
func (sm *SubagentManager) buildSubagentSystemPrompt(task *SubagentTask, cfg SubagentConfig) string {
|
||||
parentLabel := "main agent"
|
||||
if task.Depth >= 2 {
|
||||
parentLabel = "parent orchestrator"
|
||||
}
|
||||
|
||||
canSpawn := task.Depth < sm.config.MaxSpawnDepth
|
||||
canSpawn := task.Depth < cfg.MaxSpawnDepth
|
||||
|
||||
prompt := fmt.Sprintf(`# Subagent Context
|
||||
|
||||
@@ -86,7 +86,7 @@ You are a leaf worker and CANNOT spawn further sub-agents. Focus on your assigne
|
||||
|
||||
## Session Context
|
||||
- Label: %s
|
||||
- Depth: %d / %d`, task.Label, task.Depth, sm.config.MaxSpawnDepth)
|
||||
- Depth: %d / %d`, task.Label, task.Depth, cfg.MaxSpawnDepth)
|
||||
|
||||
return prompt
|
||||
}
|
||||
@@ -128,8 +128,8 @@ func (sm *SubagentManager) executeTask(ctx context.Context, task *SubagentTask)
|
||||
"status", task.Status, "iterations", iteration)
|
||||
|
||||
// Schedule auto-archive
|
||||
if sm.config.ArchiveAfterMinutes > 0 {
|
||||
go sm.scheduleArchive(task.ID, time.Duration(sm.config.ArchiveAfterMinutes)*time.Minute)
|
||||
if task.spawnConfig.ArchiveAfterMinutes > 0 {
|
||||
go sm.scheduleArchive(task.ID, time.Duration(task.spawnConfig.ArchiveAfterMinutes)*time.Minute)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -143,15 +143,15 @@ func (sm *SubagentManager) executeTask(ctx context.Context, task *SubagentTask)
|
||||
|
||||
// Build tools for subagent (no spawn/subagent tools to prevent recursion)
|
||||
toolsReg := sm.createTools()
|
||||
sm.applyDenyList(toolsReg, task.Depth)
|
||||
sm.applyDenyList(toolsReg, task.Depth, task.spawnConfig)
|
||||
|
||||
// Determine model (cascading priority matching TS sessions-spawn-tool.ts):
|
||||
// 1. Per-task model override (highest)
|
||||
// 2. SubagentConfig.Model (global subagent override)
|
||||
// 3. SubagentManager default model (inherited from parent)
|
||||
model = sm.model
|
||||
if sm.config.Model != "" {
|
||||
model = sm.config.Model
|
||||
if task.spawnConfig.Model != "" {
|
||||
model = task.spawnConfig.Model
|
||||
}
|
||||
if task.Model != "" {
|
||||
model = task.Model
|
||||
@@ -161,7 +161,7 @@ func (sm *SubagentManager) executeTask(ctx context.Context, task *SubagentTask)
|
||||
sm.emitSubagentSpanStart(traceCtx, subRootSpanID, taskStart, task, model)
|
||||
|
||||
// Build subagent system prompt (matching TS buildSubagentSystemPrompt pattern).
|
||||
systemPrompt := sm.buildSubagentSystemPrompt(task)
|
||||
systemPrompt := sm.buildSubagentSystemPrompt(task, task.spawnConfig)
|
||||
|
||||
messages := []providers.Message{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
|
||||
Reference in new issue
Block a user