fix(server): say when create_agent needs an explicit mode

This commit is contained in:
Noah
2026-11-01 16:48:34 +00:00
committed by paseo-bot[bot]
parent 3e83f433da
commit 711bf91904
4 changed files with 172 additions and 7 deletions
@@ -78,7 +78,7 @@ describe("resolveAndValidateCreateAgentMode", () => {
availableModes: OPENCODE_MODES,
}),
).toThrow(
"cannot inherit mode 'bypassPermissions' from caller (provider 'claude') for new agent (provider 'opencode'). Pass an explicit mode. Available modes for 'opencode': build, plan",
"cannot inherit mode 'bypassPermissions' from caller (provider 'claude') for new agent (provider 'opencode'). Pass an explicit mode in settings.modeId. Available modes for 'opencode': build, plan",
);
});
@@ -92,7 +92,7 @@ describe("resolveAndValidateCreateAgentMode", () => {
availableModes: CODEX_MODES,
}),
).toThrow(
"cannot inherit mode '<none>' from caller (provider 'opencode') for new agent (provider 'codex'). Pass an explicit mode. Available modes for 'codex': auto, full-access",
"cannot inherit mode '<none>' from caller (provider 'opencode') for new agent (provider 'codex'). Pass an explicit mode in settings.modeId. Available modes for 'codex': auto, full-access",
);
});
@@ -180,7 +180,7 @@ describe("resolveAndValidateCreateAgentMode", () => {
targetUnattendedMode: "full-access",
}),
).toThrow(
"cannot inherit mode 'default' from caller (provider 'claude') for new agent (provider 'codex'). Pass an explicit mode. Available modes for 'codex': auto, full-access",
"cannot inherit mode 'default' from caller (provider 'claude') for new agent (provider 'codex'). Pass an explicit mode in settings.modeId. Available modes for 'codex': auto, full-access",
);
});
@@ -195,7 +195,7 @@ describe("resolveAndValidateCreateAgentMode", () => {
targetUnattendedMode: undefined,
}),
).toThrow(
"cannot inherit mode 'bypassPermissions' from caller (provider 'claude') for new agent (provider 'zai-custom'). Pass an explicit mode. Available modes for 'zai-custom': unknown",
"cannot inherit mode 'bypassPermissions' from caller (provider 'claude') for new agent (provider 'zai-custom'). Pass an explicit mode in settings.modeId. Available modes for 'zai-custom': unknown",
);
});
@@ -77,7 +77,7 @@ export function resolveAndValidateCreateAgentMode(
}
throw new Error(
`cannot inherit mode '${formatCreateConfigParentMode(parent)}' from ${formatCreateConfigParentSource(parent)} for new agent (provider '${targetProvider}'). Pass an explicit mode. Available modes for '${targetProvider}': ${listModes(availableModes)}`,
`cannot inherit mode '${formatCreateConfigParentMode(parent)}' from ${formatCreateConfigParentSource(parent)} for new agent (provider '${targetProvider}'). Pass an explicit mode in settings.modeId. Available modes for '${targetProvider}': ${listModes(availableModes)}`,
);
}
@@ -39,7 +39,7 @@ import type {
AgentStreamEvent,
ProviderSnapshotEntry,
} from "./agent-sdk-types.js";
import type { ProviderSnapshotManager } from "./provider-snapshot-manager.js";
import { ProviderSnapshotManager } from "./provider-snapshot-manager.js";
import { createProviderSnapshotManagerStub } from "../test-utils/session-stubs.js";
import {
AgentListItemPayloadSchema,
@@ -1526,6 +1526,64 @@ describe("terminal MCP tools", () => {
});
});
const CLAUDE_CATALOG_MODES: AgentMode[] = [
{ id: "default", label: "Default" },
{ id: "bypassPermissions", label: "Bypass", isUnattended: true },
];
const CODEX_CATALOG_MODES: AgentMode[] = [
{ id: "auto", label: "Auto" },
{ id: "full-access", label: "Full Access", isUnattended: true },
];
class ModeCatalogAgentClient implements AgentClient {
readonly capabilities = {
supportsStreaming: false,
supportsSessionPersistence: false,
supportsSessionListing: false,
supportsDynamicModes: false,
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: false,
} as const;
constructor(
readonly provider: AgentProvider,
private readonly modes: AgentMode[],
) {}
async isAvailable(): Promise<boolean> {
return true;
}
async fetchCatalog() {
return { models: [], modes: this.modes };
}
async createSession(): Promise<AgentSession> {
throw new Error("ModeCatalogAgentClient only serves catalogs");
}
async resumeSession(): Promise<AgentSession> {
throw new Error("ModeCatalogAgentClient only serves catalogs");
}
}
// A real snapshot manager, so create_agent runs the provider's shared mode resolver.
function createModeCatalogManager(): ProviderSnapshotManager {
return new ProviderSnapshotManager({
logger: createTestLogger(),
providerOverrides: {
copilot: { enabled: false },
opencode: { enabled: false },
pi: { enabled: false },
},
extraClients: {
claude: new ModeCatalogAgentClient("claude", CLAUDE_CATALOG_MODES),
codex: new ModeCatalogAgentClient("codex", CODEX_CATALOG_MODES),
},
});
}
describe("create_agent MCP tool", () => {
const logger = createTestLogger();
const existingCwd = process.cwd();
@@ -3432,6 +3490,113 @@ describe("create_agent MCP tool", () => {
expect.any(Object),
);
});
it("advertises the cross-provider mode rule in the create_agent description", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
callerAgentId: "parent-agent",
logger,
});
const client = await connectInMemoryMcpClient(server);
try {
const { tools } = await client.listTools();
const createAgent = tools.find((tool) => tool.name === "create_agent")!;
expect(createAgent.description).toContain(
"A different provider does not inherit your mode: set settings.modeId from list_providers, or omit it if you run unattended so the child does too.",
);
expect(tools.map((tool) => tool.name)).toContain("list_providers");
} finally {
await client.close();
await server.close();
}
});
function claudeCaller(currentModeId: string): ManagedAgent {
return {
id: "parent-agent",
config: {},
cwd: existingCwd,
workspaceId: "wks_parent",
provider: "claude",
currentModeId,
availableModes: CLAUDE_CATALOG_MODES,
} as ManagedAgent;
}
it("refuses to carry an attended caller's mode to a different provider", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.getAgent.mockReturnValue(claudeCaller("default"));
const providerSnapshotManager = createModeCatalogManager();
try {
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager,
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(
"cannot inherit mode 'default' from caller (provider 'claude') for new agent (provider 'codex'). Pass an explicit mode in settings.modeId. Available modes for 'codex': auto, full-access",
);
expect(spies.agentManager.createAgent).not.toHaveBeenCalled();
} finally {
providerSnapshotManager.destroy();
}
});
it("gives an unattended caller's child the target provider's unattended mode", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.getAgent.mockReturnValue(claudeCaller("bypassPermissions"));
spies.agentManager.createAgent.mockResolvedValue({
id: "child-agent",
cwd: existingCwd,
lifecycle: "idle",
currentModeId: "full-access",
availableModes: CODEX_CATALOG_MODES,
config: { title: "Child" },
} as ManagedAgent);
const providerSnapshotManager = createModeCatalogManager();
try {
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager,
callerAgentId: "parent-agent",
logger,
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
...subagentCurrentWorkspace(),
title: "Child",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
});
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
expect.objectContaining({ modeId: "full-access" }),
undefined,
expect.any(Object),
);
} finally {
providerSnapshotManager.destroy();
}
});
});
const HELD_TURN_CAPABILITIES = {
@@ -1590,7 +1590,7 @@ function createToolCatalog(
{
title: "Create agent",
description:
"Create an agent. Agent-scoped creation defaults to your workspace and creates your subagent. Top-level creation without workspaceId creates a new local workspace. Requires provider/model (for example codex/gpt-5.4) and an initial prompt. Do not guess; call list_providers and list_models first if uncertain.",
"Create an agent. Agent-scoped creation defaults to your workspace and creates your subagent. Top-level creation without workspaceId creates a new local workspace. Requires provider/model (for example codex/gpt-5.4) and an initial prompt. A different provider does not inherit your mode: set settings.modeId from list_providers, or omit it if you run unattended so the child does too. Do not guess; call list_providers and list_models first if uncertain.",
inputSchema: createAgentInputSchema,
outputSchema: {
agentId: z.string(),