Multi-Agent¶
Agents work together in one of two ways, and each maps to a different use of sessions:
| How the agents work together | Use | Why |
|---|---|---|
| Delegation: an orchestrator hands a sub-agent a task and gets a result back (agent-as-tool) | A session per sub-agent | The sub-agent sees only its task and its own history. It compacts on its own, sized to its own model, and only its result goes back to the parent |
| Handoff: several agents take turns in one conversation | One shared session | Every agent should see the whole conversation, so no filtering is needed |
A session per sub-agent is how most agent frameworks isolate sub-agents, for example
Claude Code subagents, Microsoft Agent Framework's agent-as-tool, Google ADK's
AgentTool and the OpenAI Agents SDK's as_tool. It also follows the common context
engineering advice to give every sub-agent the minimum context it needs.
Branches were removed in 0.10.0
Earlier versions isolated sub-agents inside one session with ADK-style
SessionEvent.branch labels. That support was deprecated in 0.9.0 and removed in
0.10.0; see Upgrading to 0.10.0.
Session per sub-agent¶
The orchestrator exposes the sub-agent as a tool. When it is called, the tool:
- derives the sub-agent's session id from the orchestrator's session id;
- creates that session on first use, linked to the parent through metadata;
- calls the sub-agent's own
ChatClient, which has its ownSessionMemoryAdvisorand compaction settings; - returns only the sub-agent's final answer to the orchestrator.
public class ResearcherTool {
// Metadata convention linking a sub-agent session to its parent
public static final String PARENT_SESSION_ID = "parentSessionId";
public static final String AGENT = "agent";
private final SessionService sessionService;
private final ChatClient researcher;
public ResearcherTool(SessionService sessionService, ChatClient.Builder chatClientBuilder) {
this.sessionService = sessionService;
// The sub-agent has its own memory advisor, with compaction sized for its own model
this.researcher = chatClientBuilder.defaultSystem("You are a researcher. Cite every source.")
.defaultAdvisors(SessionMemoryAdvisor.builder(sessionService)
.compactionTrigger(new TurnCountTrigger(10))
.compactionStrategy(SlidingWindowCompactionStrategy.builder().maxEvents(20).build())
.build())
.build();
}
@Tool(description = "Delegate a research task to the researcher agent and return its findings")
public String research(@ToolParam(description = "The research task, with all needed context") String task,
ToolContext toolContext) {
String parentId = (String) toolContext.getContext().get(SessionMemoryAdvisor.SESSION_ID_CONTEXT_KEY);
// Stable id: the researcher remembers earlier tasks of this conversation.
// Use parentId + ":researcher:" + UUID.randomUUID() for a fresh context per task.
String childId = parentId + ":researcher";
if (this.sessionService.findById(childId) == null) {
Session parent = this.sessionService.findById(parentId);
try {
this.sessionService.create(CreateSessionRequest.builder()
.id(childId)
.userId(parent.userId())
.metadata(PARENT_SESSION_ID, parentId)
.metadata(AGENT, "researcher")
.build());
}
catch (IllegalStateException alreadyCreated) {
// a concurrent delegation created it first
}
}
// Only the task goes in and only the final answer comes back to the orchestrator
return this.researcher.prompt()
.user(task)
.advisors(a -> a.param(SessionMemoryAdvisor.SESSION_ID_CONTEXT_KEY, childId))
.call()
.content();
}
}
The orchestrator is an ordinary ChatClient call. It passes its session id to its own
advisor and, through the tool context, to the tool:
orchestrator.prompt()
.user(question)
.tools(researcherTool)
.toolContext(Map.of(SessionMemoryAdvisor.SESSION_ID_CONTEXT_KEY, sessionId))
.advisors(a -> a.param(SessionMemoryAdvisor.SESSION_ID_CONTEXT_KEY, sessionId))
.call()
.content();
To find a conversation's sub-agent sessions, filter the user's sessions on the metadata key:
List<Session> children = sessionService.findByUserId(userId)
.stream()
.filter(s -> parentId.equals(s.metadata().get(ResearcherTool.PARENT_SESSION_ID)))
.toList();
Stable or per-task session ids.
- Stable id (parentId + ":researcher"): the sub-agent remembers its earlier tasks in
the same conversation, and you can resume it later.
- Per-task id (add a random suffix): every delegation starts with a fresh context.
The orchestrator must then put everything the sub-agent needs into the task.
What you get: - the sub-agent's prompt holds only its task and its own history, not the orchestrator's conversation or other sub-agents' work; - compaction runs per session, with triggers and budgets sized to each agent's model; - each sub-agent has its own audit trail, searchable with Recall Storage; - parallel sub-agents never contend for the same session's optimistic-concurrency version.
Things to know:
- Parent link. The link between parent and child sessions is only the metadata
convention above, not a library API. Finding a parent's children means scanning
findByUserId.
- Lifecycle. Sub-agent sessions are not deleted with the parent. Give them the same
time to live as the parent (CreateSessionRequest.builder().timeToLive(...)), or delete
them yourself.
- Recall. Cross-Session Recall searches
every session of a user, including sub-agent sessions.
- Timeline. There is no single merged timeline of a multi-agent run. Merge the sessions'
events by timestamp if you need one.