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 @@ -166,7 +166,6 @@ public class SubagentsMiddleware implements HarnessRuntimeMiddleware {
// @formatter:on

private final List<SubagentEntry> baseEntries;
private volatile List<SubagentEntry> entries;
private volatile Object subagentTool;
private final TaskTool taskTool;
private final TaskRepository taskRepository;
Expand All @@ -176,6 +175,10 @@ public class SubagentsMiddleware implements HarnessRuntimeMiddleware {
private final Path mainWorkspace;
private final Function<SubagentDeclaration, SubagentFactory> factoryBuilder;
private final DefaultAgentManager agentManager;
private final WorkspaceManager workspaceManager;

private record SubagentSnapshot(
List<SubagentEntry> entries, DefaultAgentManager agentManager) {}

/**
* Optional {@link AgentGenerateTool} for LLM-driven subagent spec generation. Lazy because
Expand All @@ -195,10 +198,10 @@ public SubagentsMiddleware(
Path mainWorkspace,
Function<SubagentDeclaration, SubagentFactory> factoryBuilder) {
this.baseEntries = List.copyOf(entries);
this.entries = this.baseEntries;
this.isSessionMode = false;
DefaultAgentManager dam = new DefaultAgentManager(entries, workspaceManager);
this.agentManager = dam;
this.workspaceManager = workspaceManager;
java.util.Objects.requireNonNull(taskRepository, "taskRepository");
this.taskRepository = taskRepository;
this.subagentTool = new AgentSpawnTool(dam, taskRepository, 0);
Expand Down Expand Up @@ -226,9 +229,9 @@ public SubagentsMiddleware(
Object externalSubagentTool,
TaskRepository taskRepository) {
this.baseEntries = List.copyOf(entries);
this.entries = this.baseEntries;
this.isSessionMode = true;
this.agentManager = null;
this.workspaceManager = null;
this.subagentTool = externalSubagentTool;
java.util.Objects.requireNonNull(taskRepository, "taskRepository");
this.taskRepository = taskRepository;
Expand Down Expand Up @@ -373,7 +376,7 @@ public DefaultAgentManager getAgentManager() {
* contains an {@link AgentGenerateTool}.
*/
public List<Object> getTools() {
if (entries.isEmpty()) {
if (baseEntries.isEmpty()) {
return List.of();
}
AgentGenerateTool gen = this.agentGenerateTool;
Expand All @@ -389,7 +392,9 @@ public Flux<AgentEvent> onAgent(
RuntimeContext ctx,
AgentInput input,
Function<AgentInput, Flux<AgentEvent>> next) {
reloadSubagentEntries();
if (ctx != null) {
installSnapshot(ctx, loadSubagentSnapshot(ctx));
}
return next.apply(input);
}

Expand All @@ -399,11 +404,11 @@ public Flux<AgentEvent> onReasoning(
RuntimeContext ctx,
ReasoningInput input,
Function<ReasoningInput, Flux<AgentEvent>> next) {
List<SubagentEntry> currentEntries = this.entries;
RuntimeContext rc = ctx != null ? ctx : RuntimeContext.empty();
List<SubagentEntry> currentEntries = snapshotFor(rc).entries();
if (currentEntries.isEmpty()) {
return next.apply(input);
}
RuntimeContext rc = ctx != null ? ctx : RuntimeContext.empty();
String sessionId = rc != null ? rc.getSessionId() : null;

// ---- Phase B-3 push delivery -------------------------------------------------------
Expand Down Expand Up @@ -584,13 +589,28 @@ static List<Msg> prependToSystemMessage(List<Msg> messages, String extra) {
return out;
}

private void reloadSubagentEntries() {
private SubagentSnapshot snapshotFor(RuntimeContext runtimeContext) {
SubagentSnapshot existing = runtimeContext.get(SubagentSnapshot.class);
if (existing != null) {
return existing;
}
SubagentSnapshot snapshot = loadSubagentSnapshot(runtimeContext);
installSnapshot(runtimeContext, snapshot);
return snapshot;
}

private void installSnapshot(RuntimeContext runtimeContext, SubagentSnapshot snapshot) {
runtimeContext.put(SubagentSnapshot.class, snapshot);
runtimeContext.put(AgentSpawnTool.CTX_AGENT_MANAGER, snapshot.agentManager());
}

private SubagentSnapshot loadSubagentSnapshot(RuntimeContext runtimeContext) {
if (filesystem == null || factoryBuilder == null || isSessionMode) {
return;
return new SubagentSnapshot(baseEntries, agentManager);
}
try {
List<SubagentDeclaration> decls =
AgentSpecLoader.loadFromFilesystem(filesystem, mainWorkspace);
AgentSpecLoader.loadFromFilesystem(filesystem, runtimeContext, mainWorkspace);

List<SubagentEntry> newEntries = new ArrayList<>(baseEntries);
for (SubagentDeclaration decl : decls) {
Expand All @@ -605,13 +625,12 @@ private void reloadSubagentEntries() {
decl));
}
}

this.entries = List.copyOf(newEntries);
if (agentManager != null) {
agentManager.refreshEntries(this.entries);
}
List<SubagentEntry> snapshotEntries = List.copyOf(newEntries);
return new SubagentSnapshot(
snapshotEntries, new DefaultAgentManager(snapshotEntries, workspaceManager));
} catch (Exception e) {
log.warn("Failed to reload subagent entries from filesystem: {}", e.getMessage());
return new SubagentSnapshot(baseEntries, agentManager);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,11 @@ public static List<SubagentDeclaration> loadFromDirectory(
}

/**
* Loads subagent declarations via the {@link AbstractFilesystem}, respecting namespace
* isolation. Scans {@code subagents/} for {@code *.md} files using filesystem glob.
* Loads subagent declarations via the {@link AbstractFilesystem} using an empty runtime
* context. Scans {@code subagents/} for {@code *.md} files using filesystem glob.
*
* <p>Use {@link #loadFromFilesystem(AbstractFilesystem, RuntimeContext, Path)} when
* declarations are scoped to a calling user's namespace.
*
* @param filesystem the filesystem layer (applies namespace transparently)
* @param mainWorkspace the parent workspace for resolving relative workspace paths; may be
Expand All @@ -132,10 +135,25 @@ public static List<SubagentDeclaration> loadFromDirectory(
*/
public static List<SubagentDeclaration> loadFromFilesystem(
AbstractFilesystem filesystem, Path mainWorkspace) {
return loadFromFilesystem(filesystem, RuntimeContext.empty(), mainWorkspace);
}

/**
* Loads subagent declarations via the {@link AbstractFilesystem}, respecting the supplied
* runtime context's namespace isolation.
*
* @param filesystem the filesystem layer (applies namespace transparently)
* @param runtimeContext the call context used to resolve namespace-scoped declarations
* @param mainWorkspace the parent workspace for resolving relative workspace paths; may be
* {@code null}
* @return list of parsed declarations; never {@code null}
*/
public static List<SubagentDeclaration> loadFromFilesystem(
AbstractFilesystem filesystem, RuntimeContext runtimeContext, Path mainWorkspace) {
if (filesystem == null) {
return Collections.emptyList();
}
RuntimeContext ctx = RuntimeContext.empty();
RuntimeContext ctx = runtimeContext != null ? runtimeContext : RuntimeContext.empty();
GlobResult glob = filesystem.glob(ctx, "*.md", "subagents");
if (!glob.isSuccess() || glob.matches() == null || glob.matches().isEmpty()) {
return Collections.emptyList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,7 @@ public DefaultAgentManager(List<SubagentEntry> entries, WorkspaceManager workspa
this.workspaceManager = workspaceManager;
}

/**
* Replaces the current set of entries with a new snapshot. Called per-call from
* {@link io.agentscope.harness.agent.middleware.SubagentsMiddleware} to reflect per-user subagent
* configurations.
*/
/** Replaces the current set of entries with a new snapshot. */
public void refreshEntries(List<SubagentEntry> entries) {
Map<String, SubagentFactory> factories = new HashMap<>();
Map<String, SubagentDeclaration> decls = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ public class AgentSpawnTool {
*/
public static final String CTX_EXPOSE_TO_USER = "agentscope.subagent.expose_to_user";

/**
* {@link RuntimeContext} string key for the immutable subagent registry selected by the
* current parent-agent invocation. {@link
* io.agentscope.harness.agent.middleware.SubagentsMiddleware} installs a namespace-scoped
* manager here so concurrent callers never overwrite each other's declarations.
*/
public static final String CTX_AGENT_MANAGER = "agentscope.subagent.agent_manager";

private static final String BG_RESULT_TEMPLATE =
"""
status: accepted
Expand Down Expand Up @@ -240,23 +248,24 @@ public Mono<String> agentSpawn(
return Mono.just("Error: Maximum spawn depth exceeded (max=" + MAX_SPAWN_DEPTH + ")");
}
String canonLabel = label != null && !label.isBlank() ? label.trim() : null;
DefaultAgentManager manager = managerFor(runtimeContext);

Optional<Agent> agentOpt = agentManager.createAgentIfPresent(agentId, runtimeContext);
Optional<Agent> agentOpt = manager.createAgentIfPresent(agentId, runtimeContext);
if (agentOpt.isEmpty()) {
if (agentManager.isPrimaryOnly(agentId)) {
if (manager.isPrimaryOnly(agentId)) {
return Mono.just(
"Error: agent_id '"
+ agentId
+ "' is PRIMARY-only and cannot be spawned as a subagent.");
}
log.warn("agent_spawn unknown agentId={}, known={}", agentId, agentManager);
log.warn("agent_spawn unknown agentId={}, known={}", agentId, manager);
return Mono.just("Error: Unknown agent_id: " + agentId);
}
log.debug("agent_spawn resolved: agentId={}", agentId);
Agent agent = agentOpt.get();
String currentUserId = runtimeContext != null ? runtimeContext.getUserId() : null;
String parentSessionId = runtimeContext != null ? runtimeContext.getSessionId() : null;
var declOpt = agentManager.getDeclaration(agentId);
var declOpt = manager.getDeclaration(agentId);
boolean persist = declOpt.map(SubagentDeclaration::isPersistSession).orElse(false);

String key;
Expand Down Expand Up @@ -353,8 +362,7 @@ public Mono<String> agentSpawn(
() -> {
try {
Msg reply =
agentManager
.invokeAgent(
manager.invokeAgent(
agent,
sessionId,
currentUserId,
Expand Down Expand Up @@ -501,7 +509,8 @@ public Mono<String> agentSend(
long timeoutMs = resolveTimeoutMs(timeoutSeconds, DEFAULT_TIMEOUT_SECONDS);
String currentUserId = runtimeContext != null ? runtimeContext.getUserId() : null;
String parentSessionId = runtimeContext != null ? runtimeContext.getSessionId() : null;
var declOpt = agentManager.getDeclaration(spawned.agentId());
DefaultAgentManager manager = managerFor(runtimeContext);
var declOpt = manager.getDeclaration(spawned.agentId());
boolean remote = declOpt.map(SubagentDeclaration::isRemote).orElse(false);

if (timeoutMs == 0) {
Expand All @@ -519,8 +528,7 @@ public Mono<String> agentSend(
() -> {
try {
Msg reply =
agentManager
.invokeAgent(
manager.invokeAgent(
spawned.agent(),
spawned.sessionId(),
currentUserId,
Expand Down Expand Up @@ -592,6 +600,14 @@ public String agentList() {
// Helpers
// -----------------------------------------------------------------

private DefaultAgentManager managerFor(RuntimeContext runtimeContext) {
DefaultAgentManager scoped =
runtimeContext != null
? runtimeContext.get(CTX_AGENT_MANAGER, DefaultAgentManager.class)
: null;
return scoped != null ? scoped : agentManager;
}

/**
* Returns a {@link Mono} that invokes the local subagent.
*
Expand Down Expand Up @@ -621,6 +637,7 @@ private Mono<Msg> execLocalSync(
RuntimeContext parentCtx) {
return Mono.deferContextual(
ctxView -> {
DefaultAgentManager manager = managerFor(parentCtx);
// ── Path 1: streamEvents() — AgentEvent forwarding ──
Optional<AgentEventEmitter> emitterOpt = AgentEventEmitter.fromContext(ctxView);
if (emitterOpt.isPresent()) {
Expand All @@ -633,8 +650,7 @@ private Mono<Msg> execLocalSync(
new AgentStartEvent(spawned.sessionId(), null, spawned.agentId())
.withSource(sourcePath));

return agentManager
.invokeAgent(agent, sessionId, userId, prompt, parentCtx)
return manager.invokeAgent(agent, sessionId, userId, prompt, parentCtx)
.contextWrite(
c ->
c.put(
Expand All @@ -652,8 +668,7 @@ private Mono<Msg> execLocalSync(
SubagentEventBus bus = ctxView.get(SubagentEventBus.CONTEXT_KEY);
EventSource childSource = buildChildSource(spawned, parentCtx);

return agentManager
.invokeAgentStream(
return manager.invokeAgentStream(
agent,
sessionId,
userId,
Expand All @@ -677,13 +692,13 @@ private Mono<Msg> execLocalSync(
.switchIfEmpty(
Mono.defer(
() ->
agentManager.invokeAgent(
manager.invokeAgent(
agent, sessionId, userId, prompt,
parentCtx)));
}

// ── Path 3: non-streaming ──
return agentManager.invokeAgent(agent, sessionId, userId, prompt, parentCtx);
return manager.invokeAgent(agent, sessionId, userId, prompt, parentCtx);
});
}

Expand Down Expand Up @@ -901,7 +916,7 @@ private SpawnedAgent tryRestoreFromState(
return null;
}
Optional<Agent> agentOpt =
agentManager.createAgentIfPresent(entry.agentId(), runtimeContext);
managerFor(runtimeContext).createAgentIfPresent(entry.agentId(), runtimeContext);
if (agentOpt.isEmpty()) {
log.warn(
"Failed to restore subagent from state: agentId={} not found in registry",
Expand Down Expand Up @@ -1124,6 +1139,7 @@ private Mono<String> execSpawnTask(
long timeoutMs = resolveTimeoutMs(timeoutSeconds, DEFAULT_TIMEOUT_SECONDS);
String currentUserId = runtimeContext != null ? runtimeContext.getUserId() : null;
String parentSessionId = runtimeContext != null ? runtimeContext.getSessionId() : null;
DefaultAgentManager manager = managerFor(runtimeContext);
boolean remote = declOpt.map(SubagentDeclaration::isRemote).orElse(false);

if (timeoutMs == 0) {
Expand All @@ -1141,8 +1157,7 @@ private Mono<String> execSpawnTask(
() -> {
try {
Msg reply =
agentManager
.invokeAgent(
manager.invokeAgent(
spawned.agent(),
spawned.sessionId(),
currentUserId,
Expand Down
Loading
Loading