Clean up MCP agent orchestration inputs

Require relationship and workspace on create_agent, and reuse the same worktree target union for create_worktree. Agent-scoped prompt follow-ups now default to background finish notifications so callers can continue without polling.
This commit is contained in:
Mohamed Boudra
2026-06-20 16:59:09 +07:00
parent b8bf2345fd
commit 80fc11541f
8 changed files with 667 additions and 243 deletions
@@ -46,9 +46,7 @@ interface CreateAgentCommandDependencies {
terminalManager?: TerminalManager | null;
providerSnapshotManager: ProviderSnapshotManager;
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
// Mints a fresh workspace for a cwd and returns its id. Used when an agent is
// created with no parent and no worktree: it owns a brand-new workspace rather
// than being attributed to an existing same-cwd workspace by path.
// Mints a fresh directory workspace for a cwd and returns its id.
ensureWorkspaceForCreate?: (cwd: string) => Promise<string>;
}
@@ -81,6 +79,7 @@ export interface CreateAgentFromMcpInput {
title: string;
initialPrompt: string;
cwd?: string;
workspaceId?: string;
thinking?: string;
features?: Record<string, unknown>;
labels?: Record<string, string>;
@@ -245,15 +244,15 @@ async function resolveMcpCreateAgent(
initialPrompt: input.initialPrompt,
});
// A child agent created in its parent's working tree belongs to the parent's
// workspace. When a new worktree is created the child lives in that fresh
// workspace, so it is stamped with the new worktree's workspaceId instead
// (mirrors the session path) — keeping the agent discoverable by
// workspaceId-scoped archive. With neither a parent nor a worktree, the agent
// mints its own workspace; ownership is never resolved from cwd.
// MCP callers resolve workspace ownership before this point. Worktree
// creation wins because the new agent lives in the fresh worktree workspace.
// Otherwise use the explicit workspace id, then the parent workspace for
// direct internal callers. Ownership is never resolved from cwd.
const workspaceId = setupContinuation
? createdWorkspaceId
: (parentAgent?.workspaceId ?? (await ensureWorkspaceForMcpCreate(dependencies, resolvedCwd)));
: (input.workspaceId ??
parentAgent?.workspaceId ??
(await ensureWorkspaceForMcpCreate(dependencies, resolvedCwd)));
const { modeId: resolvedMode, featureValues: resolvedFeatures } =
await dependencies.providerSnapshotManager.resolveCreateConfig({
@@ -201,20 +201,24 @@ async function makeCwd(prefix: string): Promise<string> {
async function createTopLevelAgent(args?: Partial<StructuredContent>): Promise<string> {
const cwd = typeof args?.cwd === "string" ? args.cwd : await makeCwd("agent-cwd");
const { cwd: _cwd, ...rest } = args ?? {};
const payload = await callToolStructured(topLevelClient, "create_agent", {
cwd,
relationship: { kind: "detached" },
workspace: { kind: "create", source: { kind: "directory", path: cwd } },
title: "Parity agent",
provider: "claude/claude-test-model",
initialPrompt: "say done and stop",
settings: { modeId: "bypassPermissions" },
background: true,
...args,
...rest,
});
return str(payload.agentId);
}
async function createChildAgent(args?: Partial<StructuredContent>): Promise<string> {
const payload = await callToolStructured(agentScopedClient, "create_agent", {
relationship: { kind: "subagent" },
workspace: { kind: "current" },
title: "Parity child",
provider: "claude/claude-test-model",
initialPrompt: "say done and stop",
@@ -288,7 +292,8 @@ beforeAll(async () => {
topLevelClient = await createMcpClient(`http://127.0.0.1:${daemonHandle.port}/mcp/agents`);
const parentPayload = await callToolStructured(topLevelClient, "create_agent", {
cwd: parentAgentCwd,
relationship: { kind: "detached" },
workspace: { kind: "create", source: { kind: "directory", path: parentAgentCwd } },
title: "MCP parity parent",
provider: "claude/claude-test-model",
initialPrompt: "say done and stop",
@@ -338,10 +343,10 @@ describe("Suite A: Core Fixes", () => {
}
});
test("create_agent with detached true omits the parent agent label", async () => {
test("create_agent with detached relationship omits the parent agent label", async () => {
let agentId: string | null = null;
try {
agentId = await createChildAgent({ detached: true });
agentId = await createChildAgent({ relationship: { kind: "detached" } });
const snapshot = daemonHandle.daemon.agentManager.getAgent(agentId);
expect(snapshot?.labels?.[PARENT_AGENT_ID_LABEL]).toBeUndefined();
} finally {
@@ -836,9 +841,9 @@ describe("Suite E: Worktree Tools", () => {
const created = await callToolStructured(topLevelClient, "create_worktree", {
cwd: worktreeRepoCwd,
target: {
mode: "branch-off",
newBranch: branchName,
base: "main",
kind: "branch-off",
worktreeSlug: branchName,
baseBranch: "main",
},
});
worktreePath = str(created.worktreePath);
@@ -867,9 +872,9 @@ describe("Suite E: Worktree Tools", () => {
const created = await callToolStructured(topLevelClient, "create_worktree", {
cwd: worktreeRepoCwd,
target: {
mode: "branch-off",
newBranch: branchName,
base: "main",
kind: "branch-off",
worktreeSlug: branchName,
baseBranch: "main",
},
});
worktreePath = str(created.worktreePath);
@@ -900,9 +905,9 @@ describe("Suite E: Worktree Tools", () => {
const created = await callToolStructured(topLevelClient, "create_worktree", {
cwd: worktreeRepoCwd,
target: {
mode: "branch-off",
newBranch: branchName,
base: "main",
kind: "branch-off",
worktreeSlug: branchName,
baseBranch: "main",
},
});
worktreePath = str(created.worktreePath);
@@ -629,6 +629,29 @@ describe("terminal MCP tools", () => {
describe("create_agent MCP tool", () => {
const logger = createTestLogger();
const existingCwd = process.cwd();
const detachedDirectoryWorkspace = (path = existingCwd) => ({
relationship: { kind: "detached" as const },
workspace: { kind: "create" as const, source: { kind: "directory" as const, path } },
});
const detachedWorktreeWorkspace = (
cwd: string,
target:
| { kind: "branch-off"; worktreeSlug?: string; baseBranch?: string }
| { kind: "checkout-branch"; branch: string }
| { kind: "checkout-pr"; githubPrNumber: number },
) => ({
relationship: { kind: "detached" as const },
workspace: { kind: "create" as const, source: { kind: "worktree" as const, cwd, target } },
});
const subagentCurrentWorkspace = (cwd?: string) => ({
relationship: { kind: "subagent" as const },
workspace: { kind: "current" as const, ...(cwd ? { cwd } : {}) },
});
const detachedCurrentWorkspace = (cwd?: string) => ({
relationship: { kind: "detached" as const },
workspace: { kind: "current" as const, ...(cwd ? { cwd } : {}) },
});
const ensureWorkspaceForCreate = async () => "workspace-created";
it("requires a concise title no longer than 60 characters", async () => {
const { agentManager, agentStorage } = createTestDeps();
@@ -636,13 +659,14 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
expect(tool).toBeDefined();
const missingTitle = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
settings: { modeId: "default" },
provider: "codex/gpt-5.4",
initialPrompt: "test",
@@ -651,7 +675,7 @@ describe("create_agent MCP tool", () => {
expect(missingTitle.error.issues[0].path).toEqual(["title"]);
const tooLong = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
settings: { modeId: "default" },
provider: "codex/gpt-5.4",
title: "x".repeat(61),
@@ -661,7 +685,7 @@ describe("create_agent MCP tool", () => {
expect(tooLong.error.issues[0].path).toEqual(["title"]);
const ok = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
settings: { modeId: "default" },
provider: "codex/gpt-5.4",
title: "Short title",
@@ -676,11 +700,12 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
const parsed = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
settings: { modeId: "default" },
provider: "codex/gpt-5.4",
title: "Short title",
@@ -693,6 +718,88 @@ describe("create_agent MCP tool", () => {
).toBe(true);
});
it("requires an explicit workspace", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
const parsed = await tool.inputSchema.safeParseAsync({
relationship: { kind: "detached" },
title: "Short title",
provider: "codex/gpt-5.4",
initialPrompt: "test",
});
expect(parsed.success).toBe(false);
expect(
parsed.error.issues.some(
(issue: { path: Array<string | number> }) => issue.path[0] === "workspace",
),
).toBe(true);
});
it("rejects caller-only relationship and workspace intents without a caller agent", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
logger,
});
const tool = registeredTool(server, "create_agent");
await expect(
tool.handler({
...subagentCurrentWorkspace(),
title: "Short title",
provider: "codex/gpt-5.4",
initialPrompt: "test",
}),
).rejects.toThrow("relationship subagent requires an agent-scoped MCP session");
await expect(
tool.handler({
...detachedCurrentWorkspace(),
title: "Short title",
provider: "codex/gpt-5.4",
initialPrompt: "test",
}),
).rejects.toThrow("workspace current requires an agent-scoped MCP session");
});
it("requires a caller workspace for current workspace intent", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.getAgent.mockReturnValue({
id: "parent-agent",
cwd: existingCwd,
provider: "codex",
currentModeId: "full-access",
} as ManagedAgent);
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
callerAgentId: "parent-agent",
logger,
});
const tool = registeredTool(server, "create_agent");
await expect(
tool.handler({
...subagentCurrentWorkspace(),
title: "Child",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
}),
).rejects.toThrow("Caller agent parent-agent has no current workspace");
});
it("accepts provider features and passes them through createAgent", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.createAgent.mockResolvedValue({
@@ -708,11 +815,12 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
const input = {
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
title: "Feature test",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
@@ -732,7 +840,7 @@ describe("create_agent MCP tool", () => {
featureValues: { fast_mode: true },
}),
undefined,
undefined,
{ workspaceId: "workspace-created" },
);
});
@@ -760,11 +868,12 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
const response = await tool.handler({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
title: "Mode test",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
@@ -780,12 +889,13 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
const missingProvider = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
settings: { modeId: "default" },
title: "Short title",
initialPrompt: "test",
@@ -798,7 +908,7 @@ describe("create_agent MCP tool", () => {
).toBe(true);
const providerWithoutModel = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
settings: { modeId: "default" },
title: "Short title",
provider: "codex",
@@ -807,7 +917,7 @@ describe("create_agent MCP tool", () => {
expect(providerWithoutModel.success).toBe(false);
const providerWithEmptyModel = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
settings: { modeId: "default" },
title: "Short title",
provider: "codex/",
@@ -816,7 +926,7 @@ describe("create_agent MCP tool", () => {
expect(providerWithEmptyModel.success).toBe(false);
const providerWithEmptyProvider = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
settings: { modeId: "default" },
title: "Short title",
provider: "/gpt-5.4",
@@ -826,7 +936,7 @@ describe("create_agent MCP tool", () => {
await expect(
tool.handler({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
settings: { modeId: "default" },
title: "Short title",
provider: "codex/gpt-5.4",
@@ -836,7 +946,7 @@ describe("create_agent MCP tool", () => {
).rejects.toThrow("Unrecognized key");
});
it("accepts optional worktree intent fields in create_agent input validation", async () => {
it("accepts worktree workspace intent in create_agent input validation", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({
agentManager,
@@ -847,20 +957,19 @@ describe("create_agent MCP tool", () => {
const tool = registeredTool(server, "create_agent");
const parsed = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
...detachedWorktreeWorkspace(existingCwd, {
kind: "checkout-pr",
githubPrNumber: 42,
}),
title: "Short title",
provider: "codex/gpt-5.4",
initialPrompt: "test",
worktreeName: "review-42",
action: "checkout",
refName: "head-ref",
githubPrNumber: 42,
});
expect(parsed.success).toBe(true);
});
it("accepts each create_worktree target mode", async () => {
it("accepts each create_worktree target kind", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({
agentManager,
@@ -871,9 +980,10 @@ describe("create_agent MCP tool", () => {
const tool = registeredTool(server, "create_worktree");
for (const target of [
{ mode: "branch-off", newBranch: "feature-x", base: "main" },
{ mode: "checkout-branch", branch: "head-ref" },
{ mode: "checkout-pr", prNumber: 42 },
{ kind: "branch-off", worktreeSlug: "feature-x", baseBranch: "main" },
{ kind: "branch-off" },
{ kind: "checkout-branch", branch: "head-ref" },
{ kind: "checkout-pr", githubPrNumber: 42 },
] as const) {
const parsed = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, target });
expect(parsed.success).toBe(true);
@@ -903,13 +1013,14 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
await expect(
tool.handler({
cwd: "/path/that/does/not/exist",
...detachedDirectoryWorkspace("/path/that/does/not/exist"),
title: "Short title",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
@@ -932,11 +1043,12 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
title: " Fix auth bug ",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
@@ -948,7 +1060,7 @@ describe("create_agent MCP tool", () => {
title: "Fix auth bug",
}),
undefined,
undefined,
{ workspaceId: "workspace-created" },
);
});
@@ -967,11 +1079,12 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
title: " Fix auth ",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
@@ -982,7 +1095,7 @@ describe("create_agent MCP tool", () => {
title: "Fix auth",
}),
undefined,
undefined,
{ workspaceId: "workspace-created" },
);
});
@@ -1001,11 +1114,12 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
title: "Config test",
initialPrompt: "Do work",
provider: "codex/gpt-5.4",
@@ -1022,7 +1136,7 @@ describe("create_agent MCP tool", () => {
thinkingOptionId: "think-hard",
}),
undefined,
{ labels: { source: "mcp" } },
{ labels: { source: "mcp" }, workspaceId: "workspace-created" },
);
});
@@ -1077,12 +1191,14 @@ describe("create_agent MCP tool", () => {
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
cwd: repoDir,
...detachedWorktreeWorkspace(repoDir, {
kind: "branch-off",
worktreeSlug: "agent-worktree",
baseBranch: "main",
}),
title: "Worktree agent",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
worktreeName: "agent-worktree",
baseBranch: "main",
background: true,
});
@@ -1156,12 +1272,13 @@ describe("create_agent MCP tool", () => {
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
cwd: repoDir,
...detachedWorktreeWorkspace(repoDir, {
kind: "branch-off",
baseBranch: "main",
}),
title: "Worktree agent",
provider: "codex/gpt-5.4",
initialPrompt: "Fix workspace creation naming",
action: "branch-off",
baseBranch: "main",
background: true,
});
@@ -1241,12 +1358,13 @@ describe("create_agent MCP tool", () => {
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
cwd: repoDir,
...detachedWorktreeWorkspace(repoDir, {
kind: "checkout-branch",
branch: "existing-feature",
}),
title: "Checkout agent",
provider: "codex/gpt-5.4",
initialPrompt: "Rename this checkout from the prompt",
action: "checkout",
refName: "existing-feature",
background: true,
});
@@ -1333,11 +1451,13 @@ describe("create_agent MCP tool", () => {
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
cwd: REPO_CWD,
...detachedWorktreeWorkspace(REPO_CWD, {
kind: "checkout-pr",
githubPrNumber: 123,
}),
title: "PR agent",
provider: "codex/gpt-5.4",
initialPrompt: "Rename this PR branch from prompt",
githubPrNumber: 123,
background: true,
});
@@ -1408,7 +1528,7 @@ describe("create_agent MCP tool", () => {
const tool = registeredTool(server, "create_worktree");
const response = await tool.handler({
cwd: repoDir,
target: { mode: "branch-off", newBranch: "tool-worktree", base: "main" },
target: { kind: "branch-off", worktreeSlug: "tool-worktree", baseBranch: "main" },
});
expect(response.structuredContent.branchName).toBe("tool-worktree");
@@ -1483,7 +1603,7 @@ describe("create_agent MCP tool", () => {
const archiveTool = registeredTool(server, "archive_worktree");
const created = await createTool.handler({
cwd: repoDir,
target: { mode: "branch-off", newBranch: "archive-tool-worktree", base: "main" },
target: { kind: "branch-off", worktreeSlug: "archive-tool-worktree", baseBranch: "main" },
});
const createdWorktreePath = z.string().parse(created.structuredContent.worktreePath);
listActiveWorkspaces.mockImplementation(async () => [
@@ -1585,7 +1705,7 @@ describe("create_agent MCP tool", () => {
const archiveTool = registeredTool(server, "archive_worktree");
const created = await createTool.handler({
cwd: repoDir,
target: { mode: "branch-off", newBranch: "archive-multi-worktree", base: "main" },
target: { kind: "branch-off", worktreeSlug: "archive-multi-worktree", baseBranch: "main" },
});
const worktreePath = z.string().parse(created.structuredContent.worktreePath);
@@ -1661,7 +1781,7 @@ describe("create_agent MCP tool", () => {
const archiveTool = registeredTool(server, "archive_worktree");
const created = await createTool.handler({
cwd: repoDir,
target: { mode: "branch-off", newBranch: "archive-slug-worktree", base: "main" },
target: { kind: "branch-off", worktreeSlug: "archive-slug-worktree", baseBranch: "main" },
});
const response = await archiveTool.handler({
@@ -1736,7 +1856,7 @@ describe("create_agent MCP tool", () => {
const tool = registeredTool(server, "create_agent");
const parsed = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
title: "Custom provider agent",
settings: { modeId: "default" },
provider: "zai/custom-model",
@@ -1754,6 +1874,7 @@ describe("create_agent MCP tool", () => {
spies.agentManager.getAgent.mockReturnValue({
id: "voice-agent",
cwd: baseDir,
workspaceId: "wks_voice",
provider: "codex",
currentModeId: "full-access",
} as ManagedAgent);
@@ -1780,7 +1901,7 @@ describe("create_agent MCP tool", () => {
const tool = registeredTool(server, "create_agent");
await tool.handler({
cwd: "subdir",
...subagentCurrentWorkspace("subdir"),
title: "Child",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
@@ -1796,6 +1917,7 @@ describe("create_agent MCP tool", () => {
[PARENT_AGENT_ID_LABEL]: "voice-agent",
source: "voice",
},
workspaceId: "wks_voice",
},
);
await rm(baseDir, { recursive: true, force: true });
@@ -1806,6 +1928,7 @@ describe("create_agent MCP tool", () => {
spies.agentManager.getAgent.mockReturnValue({
id: "parent-agent",
cwd: existingCwd,
workspaceId: "wks_parent",
provider: "codex",
currentModeId: "full-access",
} as ManagedAgent);
@@ -1821,6 +1944,7 @@ describe("create_agent MCP tool", () => {
const tool = registeredTool(server, "create_agent");
await expect(
tool.handler({
...subagentCurrentWorkspace(),
title: "Child",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
@@ -1829,6 +1953,7 @@ describe("create_agent MCP tool", () => {
).rejects.toThrow(/Unrecognized key/);
const parsed = await tool.inputSchema.safeParseAsync({
...subagentCurrentWorkspace(),
title: "Child",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
@@ -1838,7 +1963,8 @@ describe("create_agent MCP tool", () => {
throw new Error("Expected caller create_agent input to parse");
}
expect(parsed.data).toMatchObject({
detached: false,
relationship: { kind: "subagent" },
workspace: { kind: "current" },
notifyOnFinish: true,
});
});
@@ -1848,6 +1974,7 @@ describe("create_agent MCP tool", () => {
const parentAgent = {
id: "parent-agent",
cwd: existingCwd,
workspaceId: "wks_parent",
provider: "codex",
currentModeId: "full-access",
} as ManagedAgent;
@@ -1876,6 +2003,7 @@ describe("create_agent MCP tool", () => {
const tool = registeredTool(server, "create_agent");
const response = await tool.handler({
...subagentCurrentWorkspace(),
title: "Child",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
@@ -1891,6 +2019,7 @@ describe("create_agent MCP tool", () => {
spies.agentManager.getAgent.mockReturnValue({
id: "parent-agent",
cwd: existingCwd,
workspaceId: "wks_parent",
provider: "codex",
currentModeId: "full-access",
} as ManagedAgent);
@@ -1913,10 +2042,10 @@ describe("create_agent MCP tool", () => {
const tool = registeredTool(server, "create_agent");
await tool.handler({
...detachedCurrentWorkspace(),
title: "Detached",
provider: "codex/gpt-5.4",
initialPrompt: "Take over",
detached: true,
labels: {
[PARENT_AGENT_ID_LABEL]: "spoofed-parent",
source: "handoff",
@@ -1932,6 +2061,7 @@ describe("create_agent MCP tool", () => {
labels: {
source: "handoff",
},
workspaceId: "wks_parent",
},
);
});
@@ -1941,6 +2071,7 @@ describe("create_agent MCP tool", () => {
spies.agentManager.getAgent.mockReturnValue({
id: "parent-agent",
cwd: existingCwd,
workspaceId: "wks_parent",
provider: "claude",
currentModeId: "bypassPermissions",
} as ManagedAgent);
@@ -1967,6 +2098,7 @@ describe("create_agent MCP tool", () => {
});
const tool = registeredTool(server, "create_agent");
const input = {
...subagentCurrentWorkspace(),
title: "Child",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
@@ -1989,6 +2121,7 @@ describe("create_agent MCP tool", () => {
labels: {
[PARENT_AGENT_ID_LABEL]: "parent-agent",
},
workspaceId: "wks_parent",
},
);
});
@@ -2018,6 +2151,7 @@ describe("create_agent MCP tool", () => {
});
const tool = registeredTool(server, "create_agent");
const result = await tool.handler({
...subagentCurrentWorkspace(),
title: "Child",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
@@ -2047,11 +2181,12 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
title: "Injected config test",
settings: { modeId: "auto" },
provider: "codex/gpt-5.4",
@@ -2065,7 +2200,7 @@ describe("create_agent MCP tool", () => {
});
expect(configArg.mcpServers).toBeUndefined();
expect(agentIdArg).toBeUndefined();
expect(optionsArg).toBeUndefined();
expect(optionsArg).toEqual({ workspaceId: "workspace-created" });
});
it("rejects an explicit mode that is not valid for the target provider", async () => {
@@ -2078,13 +2213,14 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: providerSnapshot.manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
await expect(
tool.handler({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
title: "Bad mode",
provider: "opencode/gpt-5.4",
settings: { modeId: "bypassPermissions" },
@@ -2125,12 +2261,13 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: provStub.manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
title: "Dynamic mode",
provider: "codex/gpt-5.4",
settings: { modeId: "dynamic" },
@@ -2140,7 +2277,7 @@ describe("create_agent MCP tool", () => {
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
expect.objectContaining({ modeId: "dynamic" }),
undefined,
undefined,
{ workspaceId: "workspace-created" },
);
});
@@ -2163,12 +2300,13 @@ describe("create_agent MCP tool", () => {
agentManager,
agentStorage,
providerSnapshotManager: providerSnapshot.manager,
ensureWorkspaceForCreate,
logger,
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
cwd: existingCwd,
...detachedDirectoryWorkspace(existingCwd),
title: "Legacy mode",
provider: "opencode/gpt-5.4",
settings: { modeId: "full-access" },
@@ -2178,7 +2316,7 @@ describe("create_agent MCP tool", () => {
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
expect.objectContaining({ modeId: "build", featureValues: { auto_accept: true } }),
undefined,
undefined,
{ workspaceId: "workspace-created" },
);
});
@@ -2187,6 +2325,7 @@ describe("create_agent MCP tool", () => {
const parentAgent = {
id: "parent-agent",
cwd: existingCwd,
workspaceId: "wks_parent",
provider: "claude",
currentModeId: "bypassPermissions",
} as ManagedAgent;
@@ -2214,6 +2353,7 @@ describe("create_agent MCP tool", () => {
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
...subagentCurrentWorkspace(),
title: "Child",
provider: "claude/claude-sonnet-4-20250514",
initialPrompt: "Do work",
@@ -2237,6 +2377,7 @@ describe("create_agent MCP tool", () => {
spies.agentManager.getAgent.mockReturnValue({
id: "parent-agent",
cwd: existingCwd,
workspaceId: "wks_parent",
provider: "claude",
currentModeId: "bypassPermissions",
} as ManagedAgent);
@@ -2258,6 +2399,7 @@ describe("create_agent MCP tool", () => {
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
...subagentCurrentWorkspace(),
title: "Child",
provider: "opencode/gpt-5.4",
settings: { modeId: "build" },
@@ -2272,6 +2414,151 @@ describe("create_agent MCP tool", () => {
});
});
describe("send_agent_prompt MCP tool", () => {
const logger = createTestLogger();
const existingCwd = process.cwd();
it("defaults agent-scoped prompts to background finish notifications", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
const parentAgent = {
id: "parent-agent",
cwd: existingCwd,
workspaceId: "wks_parent",
provider: "codex",
currentModeId: "full-access",
} as ManagedAgent;
const childAgent = {
id: "child-agent",
cwd: existingCwd,
lifecycle: "running",
currentModeId: null,
availableModes: [],
config: { title: "Child" },
} as ManagedAgent;
spies.agentManager.getAgent.mockImplementation((agentId: string) => {
if (agentId === "parent-agent") return parentAgent;
if (agentId === "child-agent") return childAgent;
return null;
});
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
callerAgentId: "parent-agent",
logger,
});
const tool = registeredTool(server, "send_agent_prompt");
const parsed = await tool.inputSchema.safeParseAsync({
agentId: "child-agent",
prompt: "Follow up",
});
expect(parsed.success).toBe(true);
if (!parsed.success) {
throw new Error("Expected caller send_agent_prompt input to parse");
}
expect(parsed.data).toMatchObject({
background: true,
notifyOnFinish: true,
});
const response = await tool.handler(parsed.data as Record<string, unknown>);
expect(spies.agentManager.subscribe).toHaveBeenCalledTimes(1);
expect(spies.agentManager.waitForAgentEvent).not.toHaveBeenCalled();
expect(response.structuredContent.guidance).toBe(
"You will get notified when the prompted agent finishes, errors, or needs permission. Do not call wait_for_agent or poll for status; continue with other work until the notification arrives.",
);
});
it("keeps top-level prompts blocking by default", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.getAgent.mockReturnValue({
id: "child-agent",
cwd: existingCwd,
lifecycle: "idle",
currentModeId: null,
availableModes: [],
config: { title: "Child" },
} as ManagedAgent);
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
logger,
});
const tool = registeredTool(server, "send_agent_prompt");
const parsed = await tool.inputSchema.safeParseAsync({
agentId: "child-agent",
prompt: "Follow up",
});
expect(parsed.success).toBe(true);
if (!parsed.success) {
throw new Error("Expected top-level send_agent_prompt input to parse");
}
expect(parsed.data).toMatchObject({
background: false,
notifyOnFinish: false,
});
await tool.handler(parsed.data as Record<string, unknown>);
expect(spies.agentManager.subscribe).not.toHaveBeenCalled();
expect(spies.agentManager.waitForAgentEvent).toHaveBeenCalledWith(
"child-agent",
expect.objectContaining({ waitForActive: true }),
);
});
it("does not arm a finish notification for blocking agent-scoped prompts", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
const parentAgent = {
id: "parent-agent",
cwd: existingCwd,
workspaceId: "wks_parent",
provider: "codex",
currentModeId: "full-access",
} as ManagedAgent;
const childAgent = {
id: "child-agent",
cwd: existingCwd,
lifecycle: "idle",
currentModeId: null,
availableModes: [],
config: { title: "Child" },
} as ManagedAgent;
spies.agentManager.getAgent.mockImplementation((agentId: string) => {
if (agentId === "parent-agent") return parentAgent;
if (agentId === "child-agent") return childAgent;
return null;
});
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
callerAgentId: "parent-agent",
logger,
});
const tool = registeredTool(server, "send_agent_prompt");
await invokeToolWithParsedInput(tool, {
agentId: "child-agent",
prompt: "Follow up",
background: false,
});
expect(spies.agentManager.subscribe).not.toHaveBeenCalled();
expect(spies.agentManager.waitForAgentEvent).toHaveBeenCalledWith(
"child-agent",
expect.objectContaining({ waitForActive: true }),
);
});
});
describe("update_agent MCP tool", () => {
const logger = createTestLogger();
+250 -140
View File
@@ -34,7 +34,7 @@ import {
type ArchiveDependencies,
} from "../workspace-archive-service.js";
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
import { createAgentCommand } from "./create-agent/create.js";
import { createAgentCommand, type CreateAgentFromMcpInput } from "./create-agent/create.js";
import type { VoiceCallerContext, VoiceSpeakHandler } from "../voice-types.js";
import { expandUserPath, isSameOrDescendantPath, resolvePathFromBase } from "../path-utils.js";
import type { TerminalManager } from "../../terminal/terminal-manager.js";
@@ -100,8 +100,7 @@ export interface AgentMcpServerOptions {
markWorkspaceArchiving?: ArchiveDependencies["markWorkspaceArchiving"];
clearWorkspaceArchiving?: ArchiveDependencies["clearWorkspaceArchiving"];
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
// Mints a fresh workspace for a cwd and returns its id, used when an agent is
// created with no parent and no worktree.
// Mints a fresh directory workspace for a cwd and returns its id.
ensureWorkspaceForCreate?: (cwd: string) => Promise<string>;
paseoHome?: string;
worktreesRoot?: string;
@@ -747,11 +746,91 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
.describe("Draft provider feature values."),
})
.strict();
const agentToAgentInputSchema = {
cwd: z
.string()
.optional()
.describe("Optional working directory. Defaults to your current working directory."),
const AgentRelationshipInputSchema = z.discriminatedUnion("kind", [
z
.object({ kind: z.literal("subagent") })
.strict()
.describe("Create a child agent under this agent's subagent track."),
z
.object({ kind: z.literal("detached") })
.strict()
.describe("Create a root agent that does not appear in this agent's subagent track."),
]);
const AgentCreateWorktreeTargetInputSchema = z.discriminatedUnion("kind", [
z
.object({
kind: z.literal("branch-off"),
worktreeSlug: z
.string()
.min(1)
.optional()
.describe("Optional worktree branch/slug. Omit to let Paseo generate one."),
baseBranch: z
.string()
.min(1)
.optional()
.describe("Optional base branch. Defaults to the repository default branch."),
})
.strict()
.describe("Create a new branch in a new Paseo worktree."),
z
.object({
kind: z.literal("checkout-branch"),
branch: z.string().min(1).describe("Existing branch to check out."),
})
.strict()
.describe("Check out an existing branch in a new Paseo worktree."),
z
.object({
kind: z.literal("checkout-pr"),
githubPrNumber: z.number().int().positive().describe("GitHub pull request number."),
})
.strict()
.describe("Check out a GitHub pull request in a new Paseo worktree."),
]);
const AgentWorkspaceInputSchema = z.discriminatedUnion("kind", [
z
.object({
kind: z.literal("current"),
cwd: z.string().optional().describe("Optional runtime cwd. Defaults to the caller's cwd."),
})
.strict()
.describe("Use the caller's current workspace."),
z
.object({
kind: z.literal("create"),
source: z.discriminatedUnion("kind", [
z
.object({
kind: z.literal("directory"),
path: z
.string()
.optional()
.describe("Optional directory path. Defaults to the caller's cwd."),
})
.strict(),
z
.object({
kind: z.literal("worktree"),
cwd: z
.string()
.optional()
.describe("Optional source repository. Defaults to the caller's cwd."),
target: AgentCreateWorktreeTargetInputSchema,
})
.strict(),
]),
})
.strict()
.describe("Create a new workspace for the agent."),
]);
const commonCreateAgentInputSchema = {
relationship: AgentRelationshipInputSchema.describe(
"Whether the created agent is a subagent under you or a detached root agent.",
),
workspace: AgentWorkspaceInputSchema.describe(
"Workspace ownership/location for the created agent.",
),
title: z
.string()
.trim()
@@ -770,13 +849,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
.trim()
.min(1, "initialPrompt is required")
.describe("Required first task to run immediately after creation."),
detached: z
.boolean()
.optional()
.default(false)
.describe(
"If true, the created agent stands on its own: it does not appear in your subagent track and is not archived with you.",
),
};
const agentToAgentInputSchema = {
...commonCreateAgentInputSchema,
notifyOnFinish: z
.boolean()
.optional()
@@ -785,48 +860,8 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
"Get notified when the created agent finishes, errors, or needs permission. Set false only for truly fire-and-forget agents.",
),
};
const topLevelInputSchema = {
cwd: z
.string()
.describe("Required working directory for the agent (absolute, relative, or ~)."),
title: z
.string()
.trim()
.min(1, "Title is required")
.max(60, "Title must be 60 characters or fewer")
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
provider: ProviderModelInputSchema.describe(
"Required provider/model pair, for example codex/gpt-5.4.",
),
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
settings: CreateAgentSettingsInputSchema.optional().describe(
"Initial runtime settings for the new agent.",
),
initialPrompt: z
.string()
.trim()
.min(1, "initialPrompt is required")
.describe("Required first task to run immediately after creation."),
worktreeName: z
.string()
.optional()
.describe("Optional git worktree branch name (lowercase alphanumerics + hyphen)."),
baseBranch: z
.string()
.optional()
.describe("Required when worktreeName is set: the base branch to diff/merge against."),
refName: z.string().min(1).optional().describe("Optional source ref for worktree creation."),
action: z
.enum(["branch-off", "checkout"])
.optional()
.describe("Optional worktree creation action."),
githubPrNumber: z
.number()
.int()
.positive()
.optional()
.describe("Optional GitHub pull request number to checkout."),
...commonCreateAgentInputSchema,
background: z
.boolean()
.optional()
@@ -846,6 +881,48 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
const createAgentInputSchema = callerAgentId ? agentToAgentInputSchema : topLevelInputSchema;
const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema).strict();
const topLevelCreateAgentArgsSchema = z.object(topLevelInputSchema).strict();
const commonSendAgentPromptInputSchema = {
agentId: z.string(),
prompt: z.string(),
sessionMode: z.string().optional().describe("Optional mode to set before running the prompt."),
};
const agentToAgentSendAgentPromptInputSchema = {
...commonSendAgentPromptInputSchema,
background: z
.boolean()
.optional()
.default(true)
.describe(
"Run agent in background. Agent-scoped default is true so you can continue until the finish notification arrives. Set false only when you need a blocking response.",
),
notifyOnFinish: z
.boolean()
.optional()
.default(true)
.describe(
"Get notified when the prompted agent finishes, errors, or needs permission. Set false only for truly fire-and-forget prompts.",
),
};
const topLevelSendAgentPromptInputSchema = {
...commonSendAgentPromptInputSchema,
background: z
.boolean()
.optional()
.default(false)
.describe(
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately.",
),
notifyOnFinish: z
.boolean()
.optional()
.default(false)
.describe(
"Agent-scoped only: get notified when the prompted agent finishes, errors, or needs permission.",
),
};
const sendAgentPromptInputSchema = callerAgentId
? agentToAgentSendAgentPromptInputSchema
: topLevelSendAgentPromptInputSchema;
const inspectProviderInputSchema = {
provider: ProviderOrProviderModelInputSchema.describe(
"Provider ID, optionally with a model ID (for example codex or codex/gpt-5.4).",
@@ -909,7 +986,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
{
title: "Create agent",
description:
"Create an agent tied to a working directory. Requires provider/model, for example codex/gpt-5.4. Do not guess; call list_providers and list_models first if uncertain. Optionally run an initial prompt immediately or create a git worktree for the agent.",
"Create an agent. Requires relationship, workspace, provider/model (for example codex/gpt-5.4), and an initial prompt. Do not guess; call list_providers and list_models first if uncertain.",
inputSchema: createAgentInputSchema,
outputSchema: {
agentId: z.string(),
@@ -924,19 +1001,19 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
},
},
async (args: unknown) => {
const resolvedArgs = resolveCreateAgentToolArgs(args);
const resolvedArgs = await resolveCreateAgentToolArgs(args);
const { parsedArgs, worktree } = resolvedArgs;
let requestedBackground: boolean;
let notifyOnFinish: boolean;
let detached: boolean;
if (resolvedArgs.kind === "agent-scoped") {
requestedBackground = true;
notifyOnFinish = resolvedArgs.parsedArgs.notifyOnFinish;
detached = resolvedArgs.parsedArgs.detached;
notifyOnFinish = parsedArgs.notifyOnFinish;
detached = resolvedArgs.relationship.kind === "detached";
} else {
requestedBackground = resolvedArgs.parsedArgs.background;
notifyOnFinish = resolvedArgs.parsedArgs.notifyOnFinish ?? false;
detached = false;
detached = resolvedArgs.parsedArgs.relationship.kind === "detached";
}
const {
snapshot,
@@ -961,7 +1038,8 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
provider: parsedArgs.provider,
title: parsedArgs.title,
initialPrompt: parsedArgs.initialPrompt,
cwd: parsedArgs.cwd,
cwd: resolvedArgs.cwd,
workspaceId: resolvedArgs.workspaceId,
thinking: parsedArgs.settings?.thinkingOptionId,
features: parsedArgs.settings?.features,
labels: parsedArgs.labels,
@@ -1033,48 +1111,113 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
| {
kind: "agent-scoped";
parsedArgs: AgentToAgentCreateAgentArgs;
worktree: undefined;
relationship: AgentToAgentCreateAgentArgs["relationship"];
cwd: string | undefined;
workspaceId: string | undefined;
worktree: CreateAgentFromMcpInput["worktree"];
}
| {
kind: "top-level";
parsedArgs: TopLevelCreateAgentArgs;
worktree: ReturnType<typeof resolveTopLevelCreateAgentWorktree>;
cwd: string | undefined;
workspaceId: string | undefined;
worktree: CreateAgentFromMcpInput["worktree"];
};
function resolveCreateAgentToolArgs(args: unknown): ResolvedCreateAgentToolArgs {
async function resolveCreateAgentToolArgs(args: unknown): Promise<ResolvedCreateAgentToolArgs> {
if (callerAgentId) {
const parsed = agentToAgentCreateAgentArgsSchema.parse(args);
const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsed.workspace);
return {
kind: "agent-scoped",
parsedArgs: agentToAgentCreateAgentArgsSchema.parse(args),
worktree: undefined,
parsedArgs: parsed,
relationship: parsed.relationship,
cwd,
workspaceId,
worktree,
};
}
const parsedArgs = topLevelCreateAgentArgsSchema.parse(args);
if (parsedArgs.relationship.kind === "subagent") {
throw new Error("relationship subagent requires an agent-scoped MCP session");
}
const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsedArgs.workspace);
return {
kind: "top-level",
parsedArgs,
worktree: resolveTopLevelCreateAgentWorktree(parsedArgs),
cwd,
workspaceId,
worktree,
};
}
function resolveTopLevelCreateAgentWorktree(args: TopLevelCreateAgentArgs):
| {
worktreeName?: string;
baseBranch?: string;
refName?: string;
action?: "branch-off" | "checkout";
githubPrNumber?: number;
async function resolveCreateAgentWorkspace(
workspace: AgentToAgentCreateAgentArgs["workspace"] | TopLevelCreateAgentArgs["workspace"],
): Promise<{
cwd: string | undefined;
workspaceId: string | undefined;
worktree: CreateAgentFromMcpInput["worktree"];
}> {
if (workspace.kind === "current") {
if (!callerAgentId) {
throw new Error("workspace current requires an agent-scoped MCP session");
}
| undefined {
const callerAgent = resolveCallerAgent();
if (!callerAgent?.workspaceId) {
throw new Error(`Caller agent ${callerAgentId} has no current workspace`);
}
return {
cwd: workspace.cwd,
workspaceId: callerAgent.workspaceId,
worktree: undefined,
};
}
if (workspace.source.kind === "directory") {
const cwd = resolveScopedCwd(workspace.source.path, { required: true });
if (!options.ensureWorkspaceForCreate) {
throw new Error("Workspace creation is not configured");
}
return {
cwd,
workspaceId: await options.ensureWorkspaceForCreate(cwd),
worktree: undefined,
};
}
const cwd = resolveScopedCwd(workspace.source.cwd, { required: true });
return {
worktreeName: args.worktreeName,
baseBranch: args.baseBranch,
refName: args.refName,
action: args.action,
githubPrNumber: args.githubPrNumber,
cwd,
workspaceId: undefined,
worktree: resolveCreateAgentWorktree(workspace.source.target),
};
}
function resolveCreateAgentWorktree(
target: z.infer<typeof AgentCreateWorktreeTargetInputSchema>,
): NonNullable<CreateAgentFromMcpInput["worktree"]> {
switch (target.kind) {
case "branch-off":
return {
action: "branch-off",
worktreeName: target.worktreeSlug,
baseBranch: target.baseBranch,
};
case "checkout-branch":
return {
action: "checkout",
refName: target.branch,
};
case "checkout-pr":
return {
action: "checkout",
githubPrNumber: target.githubPrNumber,
};
default:
throw new Error("unreachable");
}
}
registerTool(
"wait_for_agent",
{
@@ -1157,40 +1300,27 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
{
title: "Send agent prompt",
description:
"Send a task to a running agent. Returns immediately after the agent begins processing.",
inputSchema: {
agentId: z.string(),
prompt: z.string(),
sessionMode: z
.string()
.optional()
.describe("Optional mode to set before running the prompt."),
background: z
.boolean()
.optional()
.default(false)
.describe(
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately.",
),
notifyOnFinish: z
.boolean()
.optional()
.default(false)
.describe(
"Agent-scoped only: get notified when this run finishes, errors, or needs permission.",
),
},
"Send a task to a running agent. Agent-scoped callers run in background by default; top-level callers wait by default.",
inputSchema: sendAgentPromptInputSchema,
outputSchema: {
success: z.boolean(),
status: AgentStatusEnum,
lastMessage: z.string().nullable().optional(),
permission: AgentPermissionRequestPayloadSchema.nullable().optional(),
guidance: z.string().optional(),
},
},
async ({ agentId, prompt, sessionMode, background = false, notifyOnFinish = false }) => {
async ({
agentId,
prompt,
sessionMode,
background = Boolean(callerAgentId),
notifyOnFinish = Boolean(callerAgentId),
}) => {
if (agentManager.hasInFlightRun(agentId)) {
waitTracker.cancel(agentId, "Agent run interrupted by new prompt");
}
const shouldNotifyOnFinish = Boolean(callerAgentId && notifyOnFinish && background);
await sendPromptToAgent({
agentManager,
@@ -1201,7 +1331,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
logger: childLogger,
});
if (notifyOnFinish && callerAgentId) {
if (shouldNotifyOnFinish && callerAgentId) {
setupFinishNotification({
agentManager,
agentStorage,
@@ -1241,6 +1371,12 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
status: currentSnapshot?.lifecycle ?? "idle",
lastMessage: null,
permission: null,
...(shouldNotifyOnFinish
? {
guidance:
"You will get notified when the prompted agent finishes, errors, or needs permission. Do not call wait_for_agent or poll for status; continue with other work until the notification arrives.",
}
: {}),
};
const validJson = ensureValidJson(responseData);
@@ -2151,33 +2287,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
"Create a Paseo-managed git worktree. Branch off a new branch, check out an existing branch, or check out a GitHub PR.",
inputSchema: {
cwd: z.string().optional().describe("Repository directory. Defaults to the agent's cwd."),
target: z
.discriminatedUnion("mode", [
z
.object({
mode: z.literal("branch-off"),
newBranch: z.string().min(1).describe("Name for the new branch."),
base: z
.string()
.min(1)
.optional()
.describe("Base ref. Defaults to the repo's default branch."),
})
.describe("Create a new branch off a base."),
z
.object({
mode: z.literal("checkout-branch"),
branch: z.string().min(1).describe("Existing branch to check out."),
})
.describe("Check out an existing branch."),
z
.object({
mode: z.literal("checkout-pr"),
prNumber: z.number().int().positive().describe("Pull request number."),
})
.describe("Check out a GitHub pull request."),
])
.describe("What the worktree should contain."),
target: AgentCreateWorktreeTargetInputSchema.describe("What the worktree should contain."),
},
outputSchema: {
branchName: z.string(),
@@ -2420,9 +2530,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
}
type McpCreateWorktreeTarget =
| { mode: "branch-off"; newBranch: string; base?: string }
| { mode: "checkout-branch"; branch: string }
| { mode: "checkout-pr"; prNumber: number };
| { kind: "branch-off"; worktreeSlug?: string; baseBranch?: string }
| { kind: "checkout-branch"; branch: string }
| { kind: "checkout-pr"; githubPrNumber: number };
interface ArchiveWorktreeCommandContext {
agentManager: AgentManager;
@@ -2489,18 +2599,18 @@ function createMcpWorktreeCommandInput(
target: McpCreateWorktreeTarget,
): CreatePaseoWorktreeCommandInput {
const base = { cwd: repoRoot } as const;
switch (target.mode) {
switch (target.kind) {
case "branch-off":
return {
...base,
worktreeSlug: target.newBranch,
worktreeSlug: target.worktreeSlug,
action: "branch-off",
...(target.base ? { refName: target.base } : {}),
...(target.baseBranch ? { refName: target.baseBranch } : {}),
};
case "checkout-branch":
return { ...base, action: "checkout", refName: target.branch };
case "checkout-pr":
return { ...base, action: "checkout", githubPrNumber: target.prNumber };
return { ...base, action: "checkout", githubPrNumber: target.githubPrNumber };
default:
throw new Error("unreachable");
}