From 053da9cd07fd1ebbe2e770cd25332a748f9e50eb Mon Sep 17 00:00:00 2001 From: hujiaxin Date: Sat, 15 Aug 2026 10:32:37 +0800 Subject: [PATCH] feat(agent): let custom agents opt into parallel tool execution AgentEngine.executeToolCalls already branches to executeToolCallsParallel when AgentConfig.ParallelToolCalls is set, and that branch is fully implemented (errgroup, ordered result collection, best-effort siblings). But nothing ever set the flag: buildAgentConfig copies CustomAgentConfig into the runtime AgentConfig field by field, and this field was missing from both structs' bridge. The gate therefore always read the zero value, making executeToolCallsParallel unreachable for every custom agent. Add ParallelToolCalls to CustomAgentConfig and map it in buildAgentConfig. Defaults to false: tools are not required to be side-effect free, so agents saved before this option existed must keep running their tools sequentially. Enabling concurrency for them silently would be a behavior change rather than a fix. --- .../agent_parallel_tool_calls_config_test.go | 67 +++++++++++++++++++ .../application/service/session_agent_qa.go | 1 + internal/types/custom_agent.go | 4 ++ 3 files changed, 72 insertions(+) create mode 100644 internal/application/service/agent_parallel_tool_calls_config_test.go diff --git a/internal/application/service/agent_parallel_tool_calls_config_test.go b/internal/application/service/agent_parallel_tool_calls_config_test.go new file mode 100644 index 0000000000..4c43478c30 --- /dev/null +++ b/internal/application/service/agent_parallel_tool_calls_config_test.go @@ -0,0 +1,67 @@ +package service + +import ( + "testing" + + "github.com/Tencent/WeKnora/internal/config" + "github.com/Tencent/WeKnora/internal/types" + "github.com/stretchr/testify/require" +) + +// The engine already knows how to run a round's tool calls concurrently +// (AgentEngine.executeToolCallsParallel, gated on AgentConfig.ParallelToolCalls), +// but nothing ever set that flag: buildAgentConfig copies CustomAgentConfig into +// the runtime AgentConfig field by field and this one was absent, so the gate +// read the zero value and the parallel branch was unreachable for every custom +// agent. Failure was silent — tools just kept running one at a time. +func TestAgentConfigCarriesTheParallelToolCallsPreference(t *testing.T) { + for _, tc := range []struct { + name string + want bool + }{ + {name: "opted in", want: true}, + {name: "opted out", want: false}, + } { + t.Run(tc.name, func(t *testing.T) { + svc := &sessionService{ + cfg: &config.Config{}, + webSearchProviderRepo: &sharedAgentWebSearchRepo{}, + } + req := &types.QARequest{ + Session: &types.Session{ID: "session-1", TenantID: 1}, + CustomAgent: &types.CustomAgent{ + TenantID: 1, + Config: types.CustomAgentConfig{ + MaxIterations: 5, + ParallelToolCalls: tc.want, + }, + }, + } + + agentConfig, err := svc.buildAgentConfig(t.Context(), req, &types.Tenant{ID: 1}, 1) + require.NoError(t, err) + require.Equal(t, tc.want, agentConfig.ParallelToolCalls) + }) + } +} + +// Agents saved before this option existed must keep running their tools +// sequentially: tools are not required to be side-effect free, so silently +// turning on concurrency for them would be a behavior change, not a fix. +func TestAgentConfigDefaultsParallelToolCallsOff(t *testing.T) { + svc := &sessionService{ + cfg: &config.Config{}, + webSearchProviderRepo: &sharedAgentWebSearchRepo{}, + } + req := &types.QARequest{ + Session: &types.Session{ID: "session-1", TenantID: 1}, + CustomAgent: &types.CustomAgent{ + TenantID: 1, + Config: types.CustomAgentConfig{MaxIterations: 5}, + }, + } + + agentConfig, err := svc.buildAgentConfig(t.Context(), req, &types.Tenant{ID: 1}, 1) + require.NoError(t, err) + require.False(t, agentConfig.ParallelToolCalls) +} diff --git a/internal/application/service/session_agent_qa.go b/internal/application/service/session_agent_qa.go index c194cd26d7..8cb2d4a6bb 100644 --- a/internal/application/service/session_agent_qa.go +++ b/internal/application/service/session_agent_qa.go @@ -256,6 +256,7 @@ func (s *sessionService) buildAgentConfig( customAgent := req.CustomAgent agentConfig := &types.AgentConfig{ MaxIterations: customAgent.Config.MaxIterations, + ParallelToolCalls: customAgent.Config.ParallelToolCalls, Temperature: customAgent.Config.Temperature, WebSearchEnabled: customAgent.Config.WebSearchEnabled && req.WebSearchEnabled, WebSearchMaxResults: customAgent.Config.WebSearchMaxResults, diff --git a/internal/types/custom_agent.go b/internal/types/custom_agent.go index 6160c50dc5..01b93e61a1 100644 --- a/internal/types/custom_agent.go +++ b/internal/types/custom_agent.go @@ -132,6 +132,10 @@ type CustomAgentConfig struct { // ===== Agent Mode Settings ===== // Maximum iterations for ReAct loop (only for agent type) MaxIterations int `yaml:"max_iterations" json:"max_iterations"` + // Whether independent tool calls in one round run concurrently (default: false). + // Opt-in because tools are not required to be side-effect free; agents that + // depend on tools observing each other's writes must leave this off. + ParallelToolCalls bool `yaml:"parallel_tool_calls" json:"parallel_tool_calls,omitempty"` // Timeout for a single LLM call in seconds (0 = use global default) LLMCallTimeout int `yaml:"llm_call_timeout" json:"llm_call_timeout,omitempty"` // Allowed tools (only for agent type)