Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1353,7 +1353,16 @@ public Builder subagents(List<SubagentDeclaration> declarations) {

/** Adds a fully custom subagent factory for a given agent id. */
public Builder subagentFactory(String name, Function<String, Agent> 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<String, Agent> factory) {
this.customSubagentFactories.add(new SubagentFactoryEntry(name, description, factory));
return this;
}

Expand Down Expand Up @@ -1461,7 +1470,7 @@ public List<SubagentEntry> buildSubagentEntries(
entries.add(
new SubagentEntry(
custom.name(),
custom.name(),
custom.displayDescription(),
() -> custom.factory().apply(custom.name()),
null));
}
Expand Down Expand Up @@ -2052,7 +2061,7 @@ private List<SubagentEntry> buildStaticSubagentEntries(
entries.add(
new SubagentEntry(
custom.name(),
custom.name(),
custom.displayDescription(),
() -> custom.factory().apply(custom.name()),
null));
}
Expand Down Expand Up @@ -2430,7 +2439,14 @@ private static SkillBox staticSkillBoxFromRepos(
return box;
}

private record SubagentFactoryEntry(String name, Function<String, Agent> factory) {}
private record SubagentFactoryEntry(
String name, String description, Function<String, Agent> 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<SubagentEntry> 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<SubagentEntry> 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");
Expand Down
4 changes: 3 additions & 1 deletion docs/en/harness/subagent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down
3 changes: 2 additions & 1 deletion docs/zh/harness/subagent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`,后者可为编排器提供有意义的描述而非仅有名称(省略或为空白时回退为名称)

---

Expand Down
Loading