diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java index c573995fa..32c28e212 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java @@ -1353,7 +1353,16 @@ public Builder subagents(List declarations) { /** Adds a fully custom subagent factory for a given agent id. */ public Builder subagentFactory(String name, Function factory) { - this.customSubagentFactories.add(new SubagentFactoryEntry(name, factory)); + return subagentFactory(name, null, factory); + } + + /** + * Adds a fully custom subagent factory for a given agent id, with a description shown to + * the orchestrator. When {@code description} is null or blank, the name is used. + */ + public Builder subagentFactory( + String name, String description, Function factory) { + this.customSubagentFactories.add(new SubagentFactoryEntry(name, description, factory)); return this; } @@ -1461,7 +1470,7 @@ public List buildSubagentEntries( entries.add( new SubagentEntry( custom.name(), - custom.name(), + custom.displayDescription(), () -> custom.factory().apply(custom.name()), null)); } @@ -2052,7 +2061,7 @@ private List buildStaticSubagentEntries( entries.add( new SubagentEntry( custom.name(), - custom.name(), + custom.displayDescription(), () -> custom.factory().apply(custom.name()), null)); } @@ -2430,7 +2439,14 @@ private static SkillBox staticSkillBoxFromRepos( return box; } - private record SubagentFactoryEntry(String name, Function factory) {} + private record SubagentFactoryEntry( + String name, String description, Function factory) { + + /** Description shown to the orchestrator, falling back to the name when unset. */ + String displayDescription() { + return description != null && !description.isBlank() ? description : name; + } + } /** Marks this build as a leaf subagent (no nested subagent orchestration). */ private Builder asLeafSubagent() { diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java index 5637c3f42..948f03aaa 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.agentscope.core.agent.Agent; import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.message.Msg; import io.agentscope.core.message.MsgRole; @@ -741,6 +742,66 @@ void agentSpecLoader_markdownDeclaration_sharedMode_noPath_inlineBody() throws E "body should be inline agents body when no workspace.path"); } + // ========================================================================= + // Custom subagent factory — description (issue #1504) + // ========================================================================= + + @Test + void customSubagentFactory_usesProvidedDescription() throws Exception { + Files.createDirectories(workspace); + Agent stub = mock(Agent.class); + + List entries = + HarnessAgent.builder() + .model(stubModel("ok")) + .workspace(workspace) + .subagentFactory( + "researcher", + "Performs deep web research and summarizes findings.", + name -> stub) + .buildSubagentEntries(workspace); + + SubagentEntry entry = + entries.stream() + .filter(e -> "researcher".equals(e.name())) + .findFirst() + .orElseThrow(); + assertEquals( + "Performs deep web research and summarizes findings.", + entry.description(), + "custom factory description should be exposed to the orchestrator"); + } + + @Test + void customSubagentFactory_fallsBackToNameWhenDescriptionMissing() throws Exception { + Files.createDirectories(workspace); + Agent stub = mock(Agent.class); + + List entries = + HarnessAgent.builder() + .model(stubModel("ok")) + .workspace(workspace) + .subagentFactory("no-desc", name -> stub) + .subagentFactory("blank-desc", " ", name -> stub) + .buildSubagentEntries(workspace); + + SubagentEntry noDesc = + entries.stream().filter(e -> "no-desc".equals(e.name())).findFirst().orElseThrow(); + SubagentEntry blankDesc = + entries.stream() + .filter(e -> "blank-desc".equals(e.name())) + .findFirst() + .orElseThrow(); + assertEquals( + "no-desc", + noDesc.description(), + "null description should fall back to the subagent name"); + assertEquals( + "blank-desc", + blankDesc.description(), + "blank description should fall back to the subagent name"); + } + private static Model stubModel(String assistantText) { Model model = mock(Model.class); when(model.getModelName()).thenReturn("stub-model"); diff --git a/docs/en/harness/subagent.md b/docs/en/harness/subagent.md index 6aace54e5..3d3c15ae7 100644 --- a/docs/en/harness/subagent.md +++ b/docs/en/harness/subagent.md @@ -35,7 +35,9 @@ On every `PreReasoningEvent` turn, `SubagentsHook` injects into SYSTEM: 1. Built-in `general-purpose` 2. Programmatic declarations: `builder.subagent(SubagentDeclaration)` 3. File declarations: `workspace/subagents/*.md` (loaded non-recursively by `AgentSpecLoader`) -4. Custom factories: `builder.subagentFactory(name, factory)` +4. Custom factories: `builder.subagentFactory(name, factory)` or + `builder.subagentFactory(name, description, factory)` to give the orchestrator a meaningful + description instead of the bare name (falls back to the name when omitted or blank) --- diff --git a/docs/zh/harness/subagent.md b/docs/zh/harness/subagent.md index 309233341..6bb4b4e6d 100644 --- a/docs/zh/harness/subagent.md +++ b/docs/zh/harness/subagent.md @@ -35,7 +35,8 @@ Subagent 让主 agent 把「可独立处理、上下文重、可并行」的任 1. 内置 `general-purpose` 2. 编程声明:`builder.subagent(SubagentDeclaration)` 3. 文件声明:`workspace/subagents/*.md`(`AgentSpecLoader` 非递归加载) -4. 自定义工厂:`builder.subagentFactory(name, factory)` +4. 自定义工厂:`builder.subagentFactory(name, factory)` 或 + `builder.subagentFactory(name, description, factory)`,后者可为编排器提供有意义的描述而非仅有名称(省略或为空白时回退为名称) ---