feat(agent): add TargetAgentID to SubTurnConfig for cross-agent delegation

When TargetAgentID is set, spawnSubTurn resolves the target AgentInstance
from the registry and uses it as the base for the child turn. This gives
the child turn the target's workspace, model, tools, and system prompt
instead of inheriting from the caller.

Model validation is relaxed: empty Model is accepted when TargetAgentID
provides the model implicitly via the resolved agent instance.

Ref: #2148
This commit is contained in:
xiaoen 2026-04-15 21:27:13 +08:00
parent 2e149f44dd
commit c47f5fd2c4
2 changed files with 25 additions and 9 deletions

View file

@ -172,7 +172,10 @@ type SubTurnConfig struct {
// Used by team tool to enforce token limits across all team members. // Used by team tool to enforce token limits across all team members.
InitialTokenBudget *atomic.Int64 InitialTokenBudget *atomic.Int64
// Can be extended with temperature, topP, etc. // TargetAgentID, when set, runs the sub-turn as the specified agent.
// The target agent's workspace, model, tools, and system prompt are used
// instead of the caller's. If empty, the sub-turn runs as the parent agent.
TargetAgentID string
} }
// ====================== Context Keys ====================== // ====================== Context Keys ======================
@ -230,6 +233,7 @@ func (s *AgentLoopSpawner) SpawnSubTurn(
Critical: cfg.Critical, Critical: cfg.Critical,
Timeout: cfg.Timeout, Timeout: cfg.Timeout,
MaxContextRunes: cfg.MaxContextRunes, MaxContextRunes: cfg.MaxContextRunes,
TargetAgentID: cfg.TargetAgentID,
} }
return spawnSubTurn(ctx, s.al, parentTS, agentCfg) return spawnSubTurn(ctx, s.al, parentTS, agentCfg)
@ -312,8 +316,9 @@ func spawnSubTurn(
return nil, ErrDepthLimitExceeded return nil, ErrDepthLimitExceeded
} }
// 2. Config validation // 2. Config validation: Model is required unless TargetAgentID is set
if cfg.Model == "" { // (the target agent provides its own model).
if cfg.Model == "" && cfg.TargetAgentID == "" {
return nil, ErrInvalidSubTurnConfig return nil, ErrInvalidSubTurnConfig
} }
@ -331,12 +336,22 @@ func spawnSubTurn(
childID := al.generateSubTurnID() childID := al.generateSubTurnID()
// Get the agent instance from parent, falling back to the default agent. // Resolve the agent instance for the child turn.
// Wrap it in a shallow copy that uses an ephemeral (in-memory only) session store // When TargetAgentID is set, look up that agent from the registry so the
// so that child turns never pollute or persist to the parent's session history. // child runs with the target's workspace, model, tools, and system prompt.
baseAgent := parentTS.agent // Otherwise fall back to the parent's agent (existing behavior).
if baseAgent == nil { var baseAgent *AgentInstance
baseAgent = al.registry.GetDefaultAgent() if cfg.TargetAgentID != "" {
var ok bool
baseAgent, ok = al.registry.GetAgent(cfg.TargetAgentID)
if !ok {
return nil, fmt.Errorf("target agent %q not found in registry", cfg.TargetAgentID)
}
} else {
baseAgent = parentTS.agent
if baseAgent == nil {
baseAgent = al.registry.GetDefaultAgent()
}
} }
if baseAgent == nil { if baseAgent == nil {
return nil, errors.New("parent turnState has no agent instance") return nil, errors.New("parent turnState has no agent instance")

View file

@ -30,6 +30,7 @@ type SubTurnConfig struct {
ActualSystemPrompt string ActualSystemPrompt string
InitialMessages []providers.Message InitialMessages []providers.Message
InitialTokenBudget *atomic.Int64 // Shared token budget for team members; nil if no budget InitialTokenBudget *atomic.Int64 // Shared token budget for team members; nil if no budget
TargetAgentID string // If set, run as this agent (its workspace, model, tools)
} }
type SubagentTask struct { type SubagentTask struct {