Skip to content

Commit 92187d6

Browse files
authored
Merge pull request #1637 from krissetto/fix-subagent-logic
Fix subagent logic
2 parents d2aba4e + 957a015 commit 92187d6

4 files changed

Lines changed: 130 additions & 1 deletion

File tree

pkg/runtime/runtime.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1779,6 +1779,22 @@ func (r *LocalRuntime) handleTaskTransfer(ctx context.Context, sess *session.Ses
17791779

17801780
a := r.CurrentAgent()
17811781

1782+
// Validate that the target agent is in the current agent's sub-agents list
1783+
subAgents := a.SubAgents()
1784+
if !slices.ContainsFunc(subAgents, func(sa *agent.Agent) bool { return sa.Name() == params.Agent }) {
1785+
var subAgentNames []string
1786+
for _, sa := range subAgents {
1787+
subAgentNames = append(subAgentNames, sa.Name())
1788+
}
1789+
var errorMsg string
1790+
if len(subAgentNames) > 0 {
1791+
errorMsg = fmt.Sprintf("Agent %s cannot transfer task to %s: target agent not in sub-agents list. Available sub-agent IDs are: %s", a.Name(), params.Agent, strings.Join(subAgentNames, ", "))
1792+
} else {
1793+
errorMsg = fmt.Sprintf("Agent %s cannot transfer task to %s: target agent not in sub-agents list. This agent has no sub-agents configured.", a.Name(), params.Agent)
1794+
}
1795+
return tools.ResultError(errorMsg), nil
1796+
}
1797+
17821798
// Span for task transfer (optional)
17831799
ctx, span := r.startSpan(ctx, "runtime.task_transfer", trace.WithAttributes(
17841800
attribute.String("from.agent", a.Name()),

pkg/runtime/runtime_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,3 +1364,75 @@ func TestToolRejectionWithoutReason(t *testing.T) {
13641364
require.Equal(t, "The user rejected the tool call.", toolResponse.Response)
13651365
require.NotContains(t, toolResponse.Response, "Reason:")
13661366
}
1367+
1368+
func TestTransferTaskRejectsNonSubAgent(t *testing.T) {
1369+
// root has librarian as sub-agent but NOT planner.
1370+
// planner exists in the team. transfer_task to planner should be rejected.
1371+
prov := &mockProvider{id: "test/mock-model", stream: &mockStream{}}
1372+
1373+
librarian := agent.New("librarian", "Library agent", agent.WithModel(prov))
1374+
root := agent.New("root", "Root agent", agent.WithModel(prov))
1375+
planner := agent.New("planner", "Planner agent", agent.WithModel(prov))
1376+
1377+
agent.WithSubAgents(librarian)(root)
1378+
1379+
tm := team.New(team.WithAgents(root, planner, librarian))
1380+
1381+
rt, err := NewLocalRuntime(tm, WithSessionCompaction(false), WithModelStore(mockModelStore{}))
1382+
require.NoError(t, err)
1383+
1384+
sess := session.New(session.WithUserMessage("Test"))
1385+
evts := make(chan Event, 128)
1386+
1387+
toolCall := tools.ToolCall{
1388+
ID: "call_1",
1389+
Type: "function",
1390+
Function: tools.FunctionCall{
1391+
Name: "transfer_task",
1392+
Arguments: `{"agent":"planner","task":"do something","expected_output":""}`,
1393+
},
1394+
}
1395+
1396+
result, err := rt.handleTaskTransfer(t.Context(), sess, toolCall, evts)
1397+
require.NoError(t, err)
1398+
require.NotNil(t, result)
1399+
assert.True(t, result.IsError, "transfer to non-sub-agent should return an error result")
1400+
assert.Contains(t, result.Output, "cannot transfer task to planner")
1401+
assert.Contains(t, result.Output, "librarian")
1402+
assert.Equal(t, "root", rt.currentAgent, "current agent should remain root")
1403+
}
1404+
1405+
func TestTransferTaskAllowsSubAgent(t *testing.T) {
1406+
// Verify that transfer_task to a valid sub-agent is NOT rejected by the validation.
1407+
// We can't fully run the child session without a real model, so we just confirm
1408+
// it gets past validation (it will fail later due to mock stream being empty,
1409+
// which is fine — we only care that it's not blocked by the sub-agent check).
1410+
prov := &mockProvider{id: "test/mock-model", stream: newStreamBuilder().AddContent("done").AddStopWithUsage(10, 5).Build()}
1411+
1412+
librarian := agent.New("librarian", "Library agent", agent.WithModel(prov))
1413+
root := agent.New("root", "Root agent", agent.WithModel(prov))
1414+
1415+
agent.WithSubAgents(librarian)(root)
1416+
1417+
tm := team.New(team.WithAgents(root, librarian))
1418+
1419+
rt, err := NewLocalRuntime(tm, WithSessionCompaction(false), WithModelStore(mockModelStore{}))
1420+
require.NoError(t, err)
1421+
1422+
sess := session.New(session.WithUserMessage("Test"), session.WithToolsApproved(true))
1423+
evts := make(chan Event, 128)
1424+
1425+
toolCall := tools.ToolCall{
1426+
ID: "call_1",
1427+
Type: "function",
1428+
Function: tools.FunctionCall{
1429+
Name: "transfer_task",
1430+
Arguments: `{"agent":"librarian","task":"find a book","expected_output":"book title"}`,
1431+
},
1432+
}
1433+
1434+
result, err := rt.handleTaskTransfer(t.Context(), sess, toolCall, evts)
1435+
require.NoError(t, err)
1436+
require.NotNil(t, result)
1437+
assert.False(t, result.IsError, "transfer to valid sub-agent should succeed")
1438+
}

pkg/session/session.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ func buildInvariantSystemMessages(a *agent.Agent) []chat.Message {
449449
var messages []chat.Message
450450

451451
if a.HasSubAgents() {
452-
subAgents := append(a.SubAgents(), a.Parents()...)
452+
subAgents := a.SubAgents()
453453

454454
var text strings.Builder
455455
var validAgentIDs []string

pkg/session/session_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package session
22

33
import (
4+
"strings"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
79

810
"github.com/docker/cagent/pkg/agent"
911
"github.com/docker/cagent/pkg/chat"
@@ -394,3 +396,42 @@ func TestGetLastUserMessages(t *testing.T) {
394396
assert.Equal(t, "Third", msgs[1])
395397
})
396398
}
399+
400+
func TestTransferTaskPromptExcludesParents(t *testing.T) {
401+
t.Parallel()
402+
403+
// Build hierarchy: planner -> root -> librarian
404+
// root's sub-agents: [librarian]
405+
// root's parents: [planner] (set by planner listing root as a sub-agent)
406+
librarian := agent.New("librarian", "", agent.WithDescription("Library agent"))
407+
root := agent.New("root", "You are the root agent",
408+
agent.WithDescription("Root agent"),
409+
)
410+
planner := agent.New("planner", "",
411+
agent.WithDescription("Planner agent"),
412+
)
413+
// Connect: root -> librarian (root has librarian as sub-agent)
414+
agent.WithSubAgents(librarian)(root)
415+
// Connect: planner -> root (planner has root as sub-agent, making root's parent = planner)
416+
agent.WithSubAgents(root)(planner)
417+
418+
// Verify parent relationship was established
419+
require.Len(t, root.Parents(), 1)
420+
assert.Equal(t, "planner", root.Parents()[0].Name())
421+
422+
s := New()
423+
messages := s.GetMessages(root)
424+
425+
// Find the system message about sub-agents
426+
var subAgentMsg string
427+
for _, msg := range messages {
428+
if msg.Role == chat.MessageRoleSystem && strings.Contains(msg.Content, "transfer_task") {
429+
subAgentMsg = msg.Content
430+
break
431+
}
432+
}
433+
434+
require.NotEmpty(t, subAgentMsg, "should have a sub-agent system message")
435+
assert.Contains(t, subAgentMsg, "librarian", "should list librarian as a valid sub-agent")
436+
assert.NotContains(t, subAgentMsg, "planner", "should NOT list parent agent planner as a valid transfer target")
437+
}

0 commit comments

Comments
 (0)