@@ -58,55 +58,110 @@ func buildTaskSystemMessage(task, expectedOutput string) string {
5858 return msg
5959}
6060
61- // CurrentAgentSubAgentNames implements agenttool.Runner.
62- func (r * LocalRuntime ) CurrentAgentSubAgentNames () []string {
63- a := r .CurrentAgent ()
64- if a == nil {
65- return nil
66- }
67- return agentNames (a .SubAgents ())
61+ // SubSessionConfig describes how to build and run a child session.
62+ // Both handleTaskTransfer and RunAgent (background agents) use this
63+ // to avoid duplicating session-construction logic. Future callers
64+ // (e.g. skill-as-sub-agent) can use it as well.
65+ type SubSessionConfig struct {
66+ // Task is the user-facing task description.
67+ Task string
68+ // ExpectedOutput is an optional description of what the sub-agent should produce.
69+ ExpectedOutput string
70+ // SystemMessage, when non-empty, replaces the default task-based system
71+ // message. This is used by skill sub-agents whose system prompt is the
72+ // skill content itself rather than the team delegation boilerplate.
73+ SystemMessage string
74+ // AgentName is the name of the agent that will execute the sub-session.
75+ AgentName string
76+ // Title is a human-readable label for the sub-session (e.g. "Transferred task").
77+ Title string
78+ // ToolsApproved overrides whether tools are pre-approved in the child session.
79+ ToolsApproved bool
80+ // Thinking propagates the parent's thinking-mode flag.
81+ Thinking bool
82+ // PinAgent, when true, pins the child session to AgentName via
83+ // session.WithAgentName. This is required for concurrent background
84+ // tasks that must not share the runtime's mutable currentAgent field.
85+ PinAgent bool
86+ // ImplicitUserMessage, when non-empty, overrides the default "Please proceed."
87+ // user message sent to the child session. This allows callers like skill
88+ // sub-agents to pass the task description as the user message.
89+ ImplicitUserMessage string
6890}
6991
70- // RunAgent implements agenttool.Runner. It starts a sub-agent synchronously and
71- // blocks until completion or cancellation.
72- func (r * LocalRuntime ) RunAgent (ctx context.Context , params agenttool.RunParams ) * agenttool.RunResult {
73- child , err := r .team .Agent (params .AgentName )
74- if err != nil {
75- return & agenttool.RunResult {ErrMsg : fmt .Sprintf ("agent %q not found: %s" , params .AgentName , err )}
92+ // newSubSession builds a *session.Session from a SubSessionConfig and a parent
93+ // session. It consolidates the session options that were previously duplicated
94+ // across handleTaskTransfer and RunAgent.
95+ func newSubSession (parent * session.Session , cfg SubSessionConfig , childAgent * agent.Agent ) * session.Session {
96+ sysMsg := cfg .SystemMessage
97+ if sysMsg == "" {
98+ sysMsg = buildTaskSystemMessage (cfg .Task , cfg .ExpectedOutput )
7699 }
77100
78- sess := params .ParentSession
101+ userMsg := cfg .ImplicitUserMessage
102+ if userMsg == "" {
103+ userMsg = "Please proceed."
104+ }
79105
80- // Background tasks run with tools pre-approved because there is no user present
81- // to respond to interactive approval prompts during async execution. This is a
82- // deliberate design trade-off: the user implicitly authorises all tool calls made
83- // by the sub-agent when they approve run_background_agent. Callers should be aware
84- // that prompt injection in the sub-agent's context could exploit this gate-bypass.
85- //
86- // TODO: propagate the parent session's per-tool permission rules once the runtime
87- // supports per-session permission scoping rather than a single shared ToolsApproved flag.
88- s := session .New (
89- session .WithSystemMessage (buildTaskSystemMessage (params .Task , params .ExpectedOutput )),
90- session .WithImplicitUserMessage ("Please proceed." ),
91- session .WithMaxIterations (child .MaxIterations ()),
92- session .WithMaxConsecutiveToolCalls (child .MaxConsecutiveToolCalls ()),
93- session .WithTitle ("Background agent task" ),
94- session .WithToolsApproved (true ),
95- session .WithThinking (sess .Thinking ),
106+ opts := []session.Opt {
107+ session .WithSystemMessage (sysMsg ),
108+ session .WithImplicitUserMessage (userMsg ),
109+ session .WithMaxIterations (childAgent .MaxIterations ()),
110+ session .WithMaxConsecutiveToolCalls (childAgent .MaxConsecutiveToolCalls ()),
111+ session .WithTitle (cfg .Title ),
112+ session .WithToolsApproved (cfg .ToolsApproved ),
113+ session .WithThinking (cfg .Thinking ),
96114 session .WithSendUserMessage (false ),
97- session .WithParentID (sess .ID ),
98- session .WithAgentName (params .AgentName ),
99- )
115+ session .WithParentID (parent .ID ),
116+ }
117+ if cfg .PinAgent {
118+ opts = append (opts , session .WithAgentName (cfg .AgentName ))
119+ }
120+ return session .New (opts ... )
121+ }
122+
123+ // runSubSessionForwarding runs a child session within the parent, forwarding all
124+ // events to the caller's event channel and propagating session state (tool
125+ // approvals, thinking) back to the parent when done.
126+ //
127+ // This is the "interactive" path used by transfer_task where the parent agent
128+ // loop is blocked while the child executes.
129+ func (r * LocalRuntime ) runSubSessionForwarding (ctx context.Context , parent , child * session.Session , span trace.Span , evts chan Event , callerAgent string ) (* tools.ToolCallResult , error ) {
130+ for event := range r .RunStream (ctx , child ) {
131+ evts <- event
132+ if errEvent , ok := event .(* ErrorEvent ); ok {
133+ span .RecordError (fmt .Errorf ("%s" , errEvent .Error ))
134+ span .SetStatus (codes .Error , "sub-session error" )
135+ return nil , fmt .Errorf ("%s" , errEvent .Error )
136+ }
137+ }
100138
139+ parent .ToolsApproved = child .ToolsApproved
140+ parent .Thinking = child .Thinking
141+
142+ parent .AddSubSession (child )
143+ evts <- SubSessionCompleted (parent .ID , child , callerAgent )
144+
145+ span .SetStatus (codes .Ok , "sub-session completed" )
146+ return tools .ResultSuccess (child .GetLastAssistantMessageContent ()), nil
147+ }
148+
149+ // runSubSessionCollecting runs a child session, collecting output via an
150+ // optional content callback instead of forwarding events. This is the path
151+ // used by background agents and other non-interactive callers.
152+ //
153+ // It returns a RunResult containing either the final assistant message or
154+ // an error message.
155+ func (r * LocalRuntime ) runSubSessionCollecting (ctx context.Context , parent , child * session.Session , onContent func (string )) * agenttool.RunResult {
101156 var errMsg string
102- events := r .RunStream (ctx , s )
157+ events := r .RunStream (ctx , child )
103158 for event := range events {
104159 if ctx .Err () != nil {
105160 break
106161 }
107162 if choice , ok := event .(* AgentChoiceEvent ); ok && choice .Content != "" {
108- if params . OnContent != nil {
109- params . OnContent (choice .Content )
163+ if onContent != nil {
164+ onContent (choice .Content )
110165 }
111166 }
112167 if errEvt , ok := event .(* ErrorEvent ); ok {
@@ -123,11 +178,53 @@ func (r *LocalRuntime) RunAgent(ctx context.Context, params agenttool.RunParams)
123178 return & agenttool.RunResult {ErrMsg : errMsg }
124179 }
125180
126- result := s .GetLastAssistantMessageContent ()
127- sess .AddSubSession (s )
181+ result := child .GetLastAssistantMessageContent ()
182+ parent .AddSubSession (child )
128183 return & agenttool.RunResult {Result : result }
129184}
130185
186+ // CurrentAgentSubAgentNames implements agenttool.Runner.
187+ func (r * LocalRuntime ) CurrentAgentSubAgentNames () []string {
188+ a := r .CurrentAgent ()
189+ if a == nil {
190+ return nil
191+ }
192+ return agentNames (a .SubAgents ())
193+ }
194+
195+ // RunAgent implements agenttool.Runner. It starts a sub-agent synchronously and
196+ // blocks until completion or cancellation.
197+ func (r * LocalRuntime ) RunAgent (ctx context.Context , params agenttool.RunParams ) * agenttool.RunResult {
198+ child , err := r .team .Agent (params .AgentName )
199+ if err != nil {
200+ return & agenttool.RunResult {ErrMsg : fmt .Sprintf ("agent %q not found: %s" , params .AgentName , err )}
201+ }
202+
203+ sess := params .ParentSession
204+
205+ // Background tasks run with tools pre-approved because there is no user present
206+ // to respond to interactive approval prompts during async execution. This is a
207+ // deliberate design trade-off: the user implicitly authorises all tool calls made
208+ // by the sub-agent when they approve run_background_agent. Callers should be aware
209+ // that prompt injection in the sub-agent's context could exploit this gate-bypass.
210+ //
211+ // TODO: propagate the parent session's per-tool permission rules once the runtime
212+ // supports per-session permission scoping rather than a single shared ToolsApproved flag.
213+ cfg := SubSessionConfig {
214+ Task : params .Task ,
215+ ExpectedOutput : params .ExpectedOutput ,
216+ AgentName : params .AgentName ,
217+ Title : "Background agent task" ,
218+ ToolsApproved : true ,
219+ Thinking : sess .Thinking ,
220+ PinAgent : true ,
221+ }
222+
223+ s := newSubSession (sess , cfg , child )
224+
225+ return r .runSubSessionCollecting (ctx , sess , s , params .OnContent )
226+ }
227+
131228func (r * LocalRuntime ) handleTaskTransfer (ctx context.Context , sess * session.Session , toolCall tools.ToolCall , evts chan Event ) (* tools.ToolCallResult , error ) {
132229 var params struct {
133230 Agent string `json:"agent"`
@@ -185,41 +282,18 @@ func (r *LocalRuntime) handleTaskTransfer(ctx context.Context, sess *session.Ses
185282 return nil , err
186283 }
187284
188- s := session .New (
189- session .WithSystemMessage (buildTaskSystemMessage (params .Task , params .ExpectedOutput )),
190- session .WithImplicitUserMessage ("Please proceed." ),
191- session .WithMaxIterations (child .MaxIterations ()),
192- session .WithMaxConsecutiveToolCalls (child .MaxConsecutiveToolCalls ()),
193- session .WithTitle ("Transferred task" ),
194- session .WithToolsApproved (sess .ToolsApproved ),
195- session .WithThinking (sess .Thinking ),
196- session .WithSendUserMessage (false ),
197- session .WithParentID (sess .ID ),
198- )
199-
200- return r .runSubSession (ctx , sess , s , span , evts , a .Name ())
201- }
202-
203- // runSubSession runs a child session within the parent, forwarding events and
204- // propagating state (tool approvals, thinking) back to the parent when done.
205- func (r * LocalRuntime ) runSubSession (ctx context.Context , parent , child * session.Session , span trace.Span , evts chan Event , agentName string ) (* tools.ToolCallResult , error ) {
206- for event := range r .RunStream (ctx , child ) {
207- evts <- event
208- if errEvent , ok := event .(* ErrorEvent ); ok {
209- span .RecordError (fmt .Errorf ("%s" , errEvent .Error ))
210- span .SetStatus (codes .Error , "sub-session error" )
211- return nil , fmt .Errorf ("%s" , errEvent .Error )
212- }
285+ cfg := SubSessionConfig {
286+ Task : params .Task ,
287+ ExpectedOutput : params .ExpectedOutput ,
288+ AgentName : params .Agent ,
289+ Title : "Transferred task" ,
290+ ToolsApproved : sess .ToolsApproved ,
291+ Thinking : sess .Thinking ,
213292 }
214293
215- parent .ToolsApproved = child .ToolsApproved
216- parent .Thinking = child .Thinking
217-
218- parent .AddSubSession (child )
219- evts <- SubSessionCompleted (parent .ID , child , agentName )
294+ s := newSubSession (sess , cfg , child )
220295
221- span .SetStatus (codes .Ok , "sub-session completed" )
222- return tools .ResultSuccess (child .GetLastAssistantMessageContent ()), nil
296+ return r .runSubSessionForwarding (ctx , sess , s , span , evts , a .Name ())
223297}
224298
225299func (r * LocalRuntime ) handleHandoff (_ context.Context , _ * session.Session , toolCall tools.ToolCall , _ chan Event ) (* tools.ToolCallResult , error ) {
0 commit comments