mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 22:23:07 +00:00
fix(server): say when create_agent needs an explicit mode
This commit is contained in:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user