feat: auto-inject parentAgentId in MCP create_agent tool
When an agent calls the MCP create_agent tool to spawn a child agent, the parent-child relationship is now automatically established: - Added setManagedAgentId() to AgentSession interface (optional method) - ClaudeAgentSession stores its managed agent ID and includes it as X-Caller-Agent-Id header when connecting to the agent-control MCP - AgentManager.registerSession() calls setManagedAgentId() after registration so the session knows its ID before first prompt - MCP server extracts X-Caller-Agent-Id header from init request and stores it as callerAgentId - create_agent handler auto-injects callerAgentId as parentAgentId if not explicitly provided This enables parent agents to spawn child agents that are correctly associated, so they appear in the parent's Sub-Agents menu instead of on the homepage. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
99338d3b34
commit
b2850aea23
@@ -706,6 +706,9 @@ export class AgentManager {
|
|||||||
throw new Error(`Agent with id ${agentId} already exists`);
|
throw new Error(`Agent with id ${agentId} already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inform the session of its managed agent ID for MCP parent-child relationships
|
||||||
|
session.setManagedAgentId?.(agentId);
|
||||||
|
|
||||||
const managed = {
|
const managed = {
|
||||||
id: agentId,
|
id: agentId,
|
||||||
provider: config.provider,
|
provider: config.provider,
|
||||||
|
|||||||
@@ -180,6 +180,12 @@ export interface AgentSession {
|
|||||||
describePersistence(): AgentPersistenceHandle | null;
|
describePersistence(): AgentPersistenceHandle | null;
|
||||||
interrupt(): Promise<void>;
|
interrupt(): Promise<void>;
|
||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Set the managed agent ID for this session. This is called by AgentManager
|
||||||
|
* after registration to allow the session to include its ID in MCP requests
|
||||||
|
* (for parent-child agent relationships).
|
||||||
|
*/
|
||||||
|
setManagedAgentId?(agentId: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentClient {
|
export interface AgentClient {
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
|||||||
export interface AgentMcpServerOptions {
|
export interface AgentMcpServerOptions {
|
||||||
agentManager: AgentManager;
|
agentManager: AgentManager;
|
||||||
agentRegistry: AgentRegistry;
|
agentRegistry: AgentRegistry;
|
||||||
|
/**
|
||||||
|
* ID of the agent that is connecting to this MCP server.
|
||||||
|
* When set, create_agent will auto-inject this as parentAgentId.
|
||||||
|
*/
|
||||||
|
callerAgentId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AgentProviderEnum = z.enum(
|
const AgentProviderEnum = z.enum(
|
||||||
@@ -166,7 +171,7 @@ async function serializeSnapshotWithMetadata(
|
|||||||
export async function createAgentMcpServer(
|
export async function createAgentMcpServer(
|
||||||
options: AgentMcpServerOptions
|
options: AgentMcpServerOptions
|
||||||
): Promise<McpServer> {
|
): Promise<McpServer> {
|
||||||
const { agentManager, agentRegistry } = options;
|
const { agentManager, agentRegistry, callerAgentId } = options;
|
||||||
const waitTracker = new WaitForAgentTracker();
|
const waitTracker = new WaitForAgentTracker();
|
||||||
|
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
@@ -267,12 +272,14 @@ export async function createAgentMcpServer(
|
|||||||
|
|
||||||
const provider: AgentProvider = agentType ?? "claude";
|
const provider: AgentProvider = agentType ?? "claude";
|
||||||
const normalizedTitle = title?.trim() ?? null;
|
const normalizedTitle = title?.trim() ?? null;
|
||||||
|
// Use explicit parentAgentId if provided, otherwise default to caller agent ID
|
||||||
|
const resolvedParentAgentId = parentAgentId ?? callerAgentId;
|
||||||
const snapshot = await agentManager.createAgent({
|
const snapshot = await agentManager.createAgent({
|
||||||
provider,
|
provider,
|
||||||
cwd: resolvedCwd,
|
cwd: resolvedCwd,
|
||||||
modeId: initialMode,
|
modeId: initialMode,
|
||||||
title: normalizedTitle ?? undefined,
|
title: normalizedTitle ?? undefined,
|
||||||
parentAgentId,
|
parentAgentId: resolvedParentAgentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (initialPrompt) {
|
if (initialPrompt) {
|
||||||
|
|||||||
@@ -389,6 +389,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
private cancelCurrentTurn: (() => void) | null = null;
|
private cancelCurrentTurn: (() => void) | null = null;
|
||||||
private cachedRuntimeInfo: AgentRuntimeInfo | null = null;
|
private cachedRuntimeInfo: AgentRuntimeInfo | null = null;
|
||||||
private lastOptionsModel: string | null = null;
|
private lastOptionsModel: string | null = null;
|
||||||
|
private managedAgentId: string | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
config: ClaudeAgentConfig,
|
config: ClaudeAgentConfig,
|
||||||
@@ -650,6 +651,10 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.input = null;
|
this.input = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setManagedAgentId(agentId: string): void {
|
||||||
|
this.managedAgentId = agentId;
|
||||||
|
}
|
||||||
|
|
||||||
private async ensureQuery(): Promise<Query> {
|
private async ensureQuery(): Promise<Query> {
|
||||||
if (this.query) {
|
if (this.query) {
|
||||||
return this.query;
|
return this.query;
|
||||||
@@ -685,11 +690,16 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
|
|
||||||
// Always include the agent-control MCP server so agents can launch other agents
|
// Always include the agent-control MCP server so agents can launch other agents
|
||||||
const agentControlConfig = this.agentControlMcp ?? DEFAULT_AGENT_CONTROL_MCP;
|
const agentControlConfig = this.agentControlMcp ?? DEFAULT_AGENT_CONTROL_MCP;
|
||||||
|
// Merge base headers with the caller agent ID header for parent-child relationships
|
||||||
|
const agentControlHeaders: Record<string, string> = {
|
||||||
|
...agentControlConfig.headers,
|
||||||
|
...(this.managedAgentId ? { "X-Caller-Agent-Id": this.managedAgentId } : {}),
|
||||||
|
};
|
||||||
const defaultMcpServers: Record<string, ClaudeMcpServerConfig> = {
|
const defaultMcpServers: Record<string, ClaudeMcpServerConfig> = {
|
||||||
"agent-control": {
|
"agent-control": {
|
||||||
type: "http",
|
type: "http",
|
||||||
url: agentControlConfig.url,
|
url: agentControlConfig.url,
|
||||||
...(agentControlConfig.headers ? { headers: agentControlConfig.headers } : {}),
|
...(Object.keys(agentControlHeaders).length > 0 ? { headers: agentControlHeaders } : {}),
|
||||||
},
|
},
|
||||||
playwright: {
|
playwright: {
|
||||||
type: "stdio",
|
type: "stdio",
|
||||||
|
|||||||
@@ -636,6 +636,12 @@ class CodexAgentSession implements AgentSession {
|
|||||||
this.pendingPermissions.clear();
|
this.pendingPermissions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
setManagedAgentId(_agentId: string): void {
|
||||||
|
// Codex agents don't currently use MCP for agent-control
|
||||||
|
// This is a no-op but satisfies the AgentSession interface
|
||||||
|
}
|
||||||
|
|
||||||
private drainHistoryEvents(): AgentStreamEvent[] {
|
private drainHistoryEvents(): AgentStreamEvent[] {
|
||||||
if (!this.historyEvents.length) {
|
if (!this.historyEvents.length) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -116,11 +116,13 @@ async function main() {
|
|||||||
|
|
||||||
const agentMcpTransports: AgentMcpTransportMap = new Map();
|
const agentMcpTransports: AgentMcpTransportMap = new Map();
|
||||||
|
|
||||||
const createAgentMcpTransport = async () => {
|
const createAgentMcpTransport = async (callerAgentId?: string) => {
|
||||||
// Create a NEW McpServer instance per session (not shared across sessions)
|
// Create a NEW McpServer instance per session (not shared across sessions)
|
||||||
|
// Pass the caller agent ID so create_agent can auto-set parentAgentId
|
||||||
const agentMcpServer = await createAgentMcpServer({
|
const agentMcpServer = await createAgentMcpServer({
|
||||||
agentManager,
|
agentManager,
|
||||||
agentRegistry,
|
agentRegistry,
|
||||||
|
callerAgentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const transport = new StreamableHTTPServerTransport({
|
const transport = new StreamableHTTPServerTransport({
|
||||||
@@ -183,7 +185,9 @@ async function main() {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
transport = await createAgentMcpTransport();
|
// Extract caller agent ID from header (sent by agents when connecting)
|
||||||
|
const callerAgentId = req.header("X-Caller-Agent-Id");
|
||||||
|
transport = await createAgentMcpTransport(callerAgentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await transport.handleRequest(
|
await transport.handleRequest(
|
||||||
|
|||||||
@@ -345,13 +345,14 @@ Files requiring modification:
|
|||||||
- If issues found: add fix tasks + re-test task.
|
- If issues found: add fix tasks + re-test task.
|
||||||
- **Done (2025-12-21 22:30)**: PARTIAL PASS with issues found. Fixed infinite loop bug (added `useShallow` to `childAgents` selector). Agent screen loads correctly. Sub-Agents menu section shows "No sub-agents". Parent agent successfully created child via MCP `create_agent`. However, child appears on homepage because `parentAgentId` not set - MCP server doesn't auto-inject calling agent's ID. Fix task added.
|
- **Done (2025-12-21 22:30)**: PARTIAL PASS with issues found. Fixed infinite loop bug (added `useShallow` to `childAgents` selector). Agent screen loads correctly. Sub-Agents menu section shows "No sub-agents". Parent agent successfully created child via MCP `create_agent`. However, child appears on homepage because `parentAgentId` not set - MCP server doesn't auto-inject calling agent's ID. Fix task added.
|
||||||
|
|
||||||
- [ ] **Fix**: Auto-inject parentAgentId in MCP create_agent tool.
|
- [x] **Fix**: Auto-inject parentAgentId in MCP create_agent tool.
|
||||||
|
|
||||||
- The MCP server needs to know which agent is calling it
|
- The MCP server needs to know which agent is calling it
|
||||||
- Explore passing agent ID context when MCP transport is created
|
- Explore passing agent ID context when MCP transport is created
|
||||||
- Or: Have each agent's MCP session be scoped to that agent
|
- Or: Have each agent's MCP session be scoped to that agent
|
||||||
- Update `create_agent` handler to automatically set `parentAgentId`
|
- Update `create_agent` handler to automatically set `parentAgentId`
|
||||||
- Run typecheck after changes.
|
- Run typecheck after changes.
|
||||||
|
- **Done (2025-12-21 23:45)**: Implemented end-to-end parent-child agent ID injection. Added `setManagedAgentId()` to `AgentSession` interface (optional). `ClaudeAgentSession` stores the ID and includes it as `X-Caller-Agent-Id` header when connecting to agent-control MCP. `AgentManager.registerSession()` calls `setManagedAgentId()` after registration. MCP server extracts the header and auto-injects it as `parentAgentId` in `create_agent` handler. Typecheck passes.
|
||||||
|
|
||||||
- [ ] **Test**: Re-verify parent/child hierarchy after MCP fix.
|
- [ ] **Test**: Re-verify parent/child hierarchy after MCP fix.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user