Add MCP provider feature controls (#909)

Refs #835
This commit is contained in:
Bolun Zhang
2026-05-13 20:05:52 +08:00
committed by GitHub
parent d30a3a72da
commit 15631b815b
8 changed files with 482 additions and 65 deletions

View File

@@ -34,6 +34,17 @@ function recordArr(val: unknown): StructuredContent[] {
return z.array(z.record(z.unknown())).parse(val);
}
function expectAgentFeatureValue(snapshot: StructuredContent, featureId: string, value: unknown) {
expect(recordArr(snapshot.features)).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: featureId,
value,
}),
]),
);
}
function strArrOptional(val: unknown): string[] | undefined {
return z.array(z.string()).optional().parse(val);
}
@@ -325,6 +336,60 @@ describe("Suite A: Core Fixes", () => {
}
});
test("create_agent accepts provider features over MCP", async () => {
let agentId: string | null = null;
try {
agentId = await createTopLevelAgent({ features: { test_feature: true } });
const internalSnapshot = daemonHandle.daemon.agentManager.getAgent(agentId);
expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true });
const status = await callToolStructured(topLevelClient, "get_agent_status", { agentId });
const snapshot = z.record(z.unknown()).parse(status.snapshot);
expectAgentFeatureValue(snapshot, "test_feature", true);
} finally {
await archiveAgentIfPresent(agentId);
}
});
test("agent-scoped create_agent accepts provider features over MCP", async () => {
let agentId: string | null = null;
try {
agentId = await createChildAgent({
provider: "claude/claude-test-model",
features: { test_feature: true },
});
const internalSnapshot = daemonHandle.daemon.agentManager.getAgent(agentId);
expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true });
const status = await callToolStructured(topLevelClient, "get_agent_status", { agentId });
const snapshot = z.record(z.unknown()).parse(status.snapshot);
expectAgentFeatureValue(snapshot, "test_feature", true);
} finally {
await archiveAgentIfPresent(agentId);
}
});
test("set_agent_feature updates provider features over MCP", async () => {
let agentId: string | null = null;
try {
agentId = await createTopLevelAgent({ features: { test_feature: false } });
const updated = await callToolStructured(topLevelClient, "set_agent_feature", {
agentId,
featureId: "test_feature",
value: true,
});
expect(updated.success).toBe(true);
const internalSnapshot = daemonHandle.daemon.agentManager.getAgent(agentId);
expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true });
const status = await callToolStructured(topLevelClient, "get_agent_status", { agentId });
const snapshot = z.record(z.unknown()).parse(status.snapshot);
expectAgentFeatureValue(snapshot, "test_feature", true);
} finally {
await archiveAgentIfPresent(agentId);
}
});
test("create_agent accepts labels param", async () => {
let agentId: string | null = null;
try {
@@ -480,6 +545,7 @@ describe("Suite C: Schedule Tools", () => {
prompt: "say hello",
every: "5m",
name: "Parity schedule list",
provider: "claude",
});
scheduleId = str(created.id);
@@ -527,6 +593,7 @@ describe("Suite C: Schedule Tools", () => {
prompt: "say hello",
every: "5m",
name: "Parity inspect schedule",
provider: "claude",
});
scheduleId = str(created.id);
@@ -551,6 +618,7 @@ describe("Suite C: Schedule Tools", () => {
prompt: "say hello",
every: "5m",
name: "Parity pause schedule",
provider: "claude",
});
scheduleId = str(created.id);
@@ -577,6 +645,7 @@ describe("Suite C: Schedule Tools", () => {
prompt: "say hello",
every: "5m",
name: "Parity delete schedule",
provider: "claude",
});
scheduleId = str(created.id);

View File

@@ -47,6 +47,20 @@ interface LooseStructuredContent {
interface RegisteredMcpTool {
inputSchema: LooseInputSchema;
callback?: (
input: unknown,
extra?: unknown,
) => Promise<{
structuredContent: LooseStructuredContent;
content?: Array<{ type: string; text?: string }>;
}>;
handler?: (input: unknown) => Promise<{
structuredContent: LooseStructuredContent;
content?: Array<{ type: string; text?: string }>;
}>;
}
interface RegisteredMcpToolWithHandler extends RegisteredMcpTool {
handler: (input: unknown) => Promise<{
structuredContent: LooseStructuredContent;
content?: Array<{ type: string; text?: string }>;
@@ -64,12 +78,16 @@ function lookupTool(
function registeredTool(
server: Awaited<ReturnType<typeof createAgentMcpServer>>,
name: string,
): RegisteredMcpTool {
): RegisteredMcpToolWithHandler {
const tool = lookupTool(server, name);
if (!tool) {
throw new Error(`MCP tool not registered: ${name}`);
}
return tool;
const handler = tool.handler ?? tool.callback;
if (!handler) {
throw new Error(`MCP tool has no callable handler: ${name}`);
}
return { ...tool, handler };
}
function agentsOf(response: {
@@ -96,6 +114,7 @@ function buildAgentManagerSpies() {
waitForAgentEvent: vi.fn(),
recordUserMessage: vi.fn(),
setAgentMode: vi.fn(),
setAgentFeature: vi.fn().mockResolvedValue(undefined),
setLabels: vi.fn().mockResolvedValue(undefined),
setTitle: vi.fn().mockResolvedValue(undefined),
archiveAgent: vi.fn().mockResolvedValue({ archivedAt: new Date().toISOString() }),
@@ -467,6 +486,44 @@ describe("create_agent MCP tool", () => {
).toBe(true);
});
it("accepts provider features and passes them through createAgent", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.createAgent.mockResolvedValue({
id: "feature-agent",
cwd: REPO_CWD,
lifecycle: "idle",
currentModeId: null,
availableModes: [],
config: { title: "Feature test", featureValues: { fast_mode: true } },
} as ManagedAgent);
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
const tool = registeredTool(server, "create_agent");
const input = {
cwd: existingCwd,
title: "Feature test",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
background: true,
features: { fast_mode: true },
};
const parsed = await tool.inputSchema.safeParseAsync(input);
expect(parsed.success).toBe(true);
await tool.handler(input);
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
expect.objectContaining({
provider: "codex",
model: "gpt-5.4",
featureValues: { fast_mode: true },
}),
undefined,
undefined,
);
});
it("requires provider as provider/model and rejects the old model field", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
@@ -1269,6 +1326,58 @@ describe("create_agent MCP tool", () => {
await rm(baseDir, { recursive: true, force: true });
});
it("accepts provider features from caller agents and passes them through createAgent", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.getAgent.mockReturnValue({
id: "parent-agent",
cwd: existingCwd,
provider: "claude",
currentModeId: "bypassPermissions",
} as ManagedAgent);
spies.agentManager.createAgent.mockResolvedValue({
id: "child-agent",
cwd: existingCwd,
lifecycle: "idle",
currentModeId: null,
availableModes: [],
config: { title: "Child", featureValues: { fast_mode: true } },
} as ManagedAgent);
const server = await createAgentMcpServer({
agentManager,
agentStorage,
callerAgentId: "parent-agent",
logger,
});
const tool = registeredTool(server, "create_agent");
const input = {
title: "Child",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
background: true,
features: { fast_mode: true },
};
const parsed = await tool.inputSchema.safeParseAsync(input);
expect(parsed.success).toBe(true);
await tool.handler(input);
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
expect.objectContaining({
provider: "codex",
model: "gpt-5.4",
featureValues: { fast_mode: true },
}),
undefined,
{
labels: {
[PARENT_AGENT_ID_LABEL]: "parent-agent",
},
},
);
});
it("delegates MCP injection to AgentManager and passes through an undefined agent ID", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.createAgent.mockResolvedValue({
@@ -1465,6 +1574,29 @@ describe("create_agent MCP tool", () => {
});
});
describe("set_agent_feature MCP tool", () => {
const logger = createTestLogger();
it("sets a provider feature on an existing agent", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
const tool = registeredTool(server, "set_agent_feature");
const input = {
agentId: "agent-1",
featureId: "fast_mode",
value: true,
};
const parsed = await tool.inputSchema.safeParseAsync(input);
expect(parsed.success).toBe(true);
const response = await tool.handler(input);
expect(spies.agentManager.setAgentFeature).toHaveBeenCalledWith("agent-1", "fast_mode", true);
expect(response.structuredContent).toEqual({ success: true });
});
});
describe("create_schedule MCP tool", () => {
const logger = createTestLogger();

View File

@@ -489,6 +489,10 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
"Required provider/model pair, for example codex/gpt-5.4.",
),
thinking: z.string().optional().describe("Thinking option ID"),
features: z
.record(z.unknown())
.optional()
.describe("Provider-specific feature values, for example { fast_mode: true } for Codex."),
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
initialPrompt: z
.string()
@@ -531,6 +535,10 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
"Required provider/model pair, for example codex/gpt-5.4.",
),
thinking: z.string().optional().describe("Thinking option ID"),
features: z
.record(z.unknown())
.optional()
.describe("Provider-specific feature values, for example { fast_mode: true } for Codex."),
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
initialPrompt: z
.string()
@@ -630,6 +638,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
normalizedTitle: string | null;
model: string | undefined;
thinking: string | undefined;
features: Record<string, unknown> | undefined;
labels: Record<string, string> | undefined;
notifyOnFinish: boolean;
resolvedCwd: string;
@@ -699,6 +708,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
normalizedTitle: callerArgs.title.trim(),
model: resolvedProviderModel.model,
thinking: callerArgs.thinking,
features: callerArgs.features,
labels: callerArgs.labels,
notifyOnFinish: callerArgs.notifyOnFinish ?? false,
resolvedCwd,
@@ -772,6 +782,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
normalizedTitle: topLevelArgs.title.trim(),
model: resolvedProviderModel.model,
thinking: topLevelArgs.thinking,
features: topLevelArgs.features,
labels: topLevelArgs.labels,
notifyOnFinish: topLevelArgs.notifyOnFinish ?? false,
resolvedCwd,
@@ -815,6 +826,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
normalizedTitle,
model,
thinking,
features,
labels,
notifyOnFinish,
resolvedCwd,
@@ -836,6 +848,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
title: normalizedTitle ?? undefined,
model,
thinkingOptionId: thinking,
featureValues: features,
},
undefined,
Object.keys(mergedLabels).length > 0 ? { labels: mergedLabels } : undefined,
@@ -923,6 +936,29 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
},
);
server.registerTool(
"set_agent_feature",
{
title: "Set agent feature",
description: "Set a provider-specific feature on an existing agent, such as Codex fast_mode.",
inputSchema: {
agentId: z.string(),
featureId: z.string().trim().min(1),
value: z.unknown(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId, featureId, value }) => {
await agentManager.setAgentFeature(agentId, featureId, value);
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
server.registerTool(
"wait_for_agent",
{

View File

@@ -1,3 +1,4 @@
import pino from "pino";
import { describe, expect, test } from "vitest";
import type { AgentSession, AgentSessionConfig } from "../agent-sdk-types.js";
@@ -33,6 +34,25 @@ const TEST_COLLABORATION_MODES: CollaborationModeRecord[] = [
type CodexFeaturesTestSession = AgentSession;
interface CapturedLogEntry {
level?: number;
msg?: string;
[key: string]: unknown;
}
function createCapturedLogger(): { logger: pino.Logger; entries: CapturedLogEntry[] } {
const entries: CapturedLogEntry[] = [];
const logger = pino(
{ level: "debug" },
{
write(line: string) {
entries.push(JSON.parse(line) as CapturedLogEntry);
},
},
);
return { logger, entries };
}
function createConfig(overrides: Partial<AgentSessionConfig> = {}): AgentSessionConfig {
return {
provider: CODEX_PROVIDER,
@@ -43,7 +63,10 @@ function createConfig(overrides: Partial<AgentSessionConfig> = {}): AgentSession
};
}
function createSessionHarness(configOverrides: Partial<AgentSessionConfig> = {}): {
function createSessionHarness(
configOverrides: Partial<AgentSessionConfig> = {},
options: { logger?: pino.Logger } = {},
): {
session: CodexFeaturesTestSession;
appServer: FakeCodexAppServer;
} {
@@ -54,17 +77,20 @@ function createSessionHarness(configOverrides: Partial<AgentSessionConfig> = {})
const session = new __codexAppServerInternals.CodexAppServerAgentSession(
{ ...config, provider: CODEX_PROVIDER },
null,
createTestLogger(),
options.logger ?? createTestLogger(),
async () => appServer.child,
) as CodexFeaturesTestSession;
return { session, appServer };
}
async function createConnectedSession(configOverrides: Partial<AgentSessionConfig> = {}): Promise<{
async function createConnectedSession(
configOverrides: Partial<AgentSessionConfig> = {},
options: { logger?: pino.Logger } = {},
): Promise<{
session: CodexFeaturesTestSession;
appServer: FakeCodexAppServer;
}> {
const harness = createSessionHarness(configOverrides);
const harness = createSessionHarness(configOverrides, options);
await harness.session.connect();
harness.appServer.assertNoErrors();
return harness;
@@ -136,6 +162,30 @@ describe("Codex app-server provider features", () => {
]);
});
test("constructor ignores restored fast mode when model does not support it", async () => {
const { session, appServer } = await createConnectedSession({
model: "gpt-3.5-turbo",
featureValues: { fast_mode: true },
});
expect(session.features).toEqual([
{
type: "toggle",
id: "plan_mode",
label: "Plan",
description: "Switch Codex into planning-only collaboration mode",
tooltip: "Toggle plan mode",
icon: "list-todo",
value: false,
},
]);
await session.startTurn("hello");
await expect(appServer.waitForTurnStart()).resolves.not.toMatchObject({
serviceTier: expect.anything(),
});
});
test("setFeature('fast_mode', true) sets serviceTier to fast", async () => {
const { session, appServer } = await createConnectedSession();
@@ -160,6 +210,14 @@ describe("Codex app-server provider features", () => {
});
});
test("setFeature('fast_mode', true) rejects models that do not support fast mode", async () => {
const { session } = await createConnectedSession({ model: "gpt-3.5-turbo" });
await expect(session.setFeature?.("fast_mode", true)).rejects.toThrow(
"Codex fast mode is not available for model 'gpt-3.5-turbo'",
);
});
test("setFeature invalidates runtime info", async () => {
const { session } = await createConnectedSession();
@@ -228,6 +286,30 @@ describe("Codex app-server provider features", () => {
});
});
test("startTurn logs a sanitized turn/start summary for fast mode observability", async () => {
const capture = createCapturedLogger();
const prompt = "secret prompt text should not be logged";
const { session } = await createConnectedSession(
{ featureValues: { fast_mode: true } },
{ logger: capture.logger },
);
await session.startTurn(prompt);
const entry = capture.entries.find(
(candidate) => candidate.msg === "Starting Codex app-server turn",
);
expect(entry).toMatchObject({
level: 30,
msg: "Starting Codex app-server turn",
model: "gpt-5.4",
modeId: "auto",
serviceTier: "fast",
cwd: "/tmp/codex-fast-mode-test",
});
expect(JSON.stringify(entry)).not.toContain(prompt);
});
test("setModel clears fast mode when switching to an unsupported model", async () => {
const { session, appServer } = await createConnectedSession();

View File

@@ -2714,7 +2714,7 @@ class CodexAppServerAgentSession implements AgentSession {
this.currentMode = config.modeId;
this.config = config;
this.config.thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
if (this.config.featureValues?.fast_mode) {
if (this.config.featureValues?.fast_mode && codexModelSupportsFastMode(this.config.model)) {
this.serviceTier = "fast";
}
if (this.config.featureValues?.plan_mode) {
@@ -3109,50 +3109,18 @@ class CodexAppServerAgentSession implements AgentSession {
return args ? `$${commandName} ${args}` : `$${commandName}`;
}
async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> {
return runProviderTurn({
prompt,
runOptions: options,
startTurn: (p, o) => this.startTurn(p, o),
subscribe: (callback) => this.subscribe(callback),
getSessionId: async () => (await this.getRuntimeInfo()).sessionId ?? "",
reduceFinalText: ({ current, item }) => {
if (item.type === "assistant_message") {
return item.text;
}
if (item.type === "tool_call" && item.detail.type === "plan") {
return item.detail.text;
}
return current;
},
});
}
async startTurn(
prompt: AgentPromptInput,
private async buildTurnStartParams(
prompt: CodexPromptInput,
options?: AgentRunOptions,
): Promise<{ turnId: string }> {
if (this.activeForegroundTurnId) {
throw new Error("A foreground turn is already active");
}
await this.connect();
if (!this.client) {
throw new Error("Codex client not initialized");
}
const slashCommand = await this.resolveSlashCommandInvocation(prompt);
const effectivePrompt = slashCommand
? await this.buildCommandPromptInput(slashCommand.commandName, slashCommand.args)
: prompt;
if (this.currentThreadId) {
await this.ensureThreadLoaded();
} else {
await this.ensureThread();
}
const input = await this.buildUserInput(effectivePrompt);
): Promise<{
params: Record<string, unknown>;
thinkingOptionId?: string;
approvalPolicy: string;
sandboxPolicyType: string;
hasOutputSchema: boolean;
hasCodexConfig: boolean;
}> {
const input = await this.buildUserInput(prompt);
const preset = MODE_PRESETS[this.currentMode] ?? MODE_PRESETS[DEFAULT_CODEX_MODE_ID];
const approvalPolicy = this.config.approvalPolicy ?? preset.approvalPolicy;
const sandboxPolicyType = this.config.sandboxMode ?? preset.sandbox;
@@ -3200,11 +3168,109 @@ class CodexAppServerAgentSession implements AgentSession {
params.config = codexConfig;
}
return {
params,
thinkingOptionId,
approvalPolicy,
sandboxPolicyType,
hasOutputSchema: Boolean(options?.outputSchema),
hasCodexConfig: Boolean(codexConfig),
};
}
private logTurnStartSummary({
turnId,
thinkingOptionId,
approvalPolicy,
sandboxPolicyType,
hasOutputSchema,
hasCodexConfig,
}: {
turnId: string;
thinkingOptionId?: string;
approvalPolicy: string;
sandboxPolicyType: string;
hasOutputSchema: boolean;
hasCodexConfig: boolean;
}): void {
this.logger.info(
{
turnId,
threadId: this.currentThreadId,
model: this.config.model ?? null,
modeId: this.currentMode ?? null,
effort: thinkingOptionId ?? null,
serviceTier: this.serviceTier,
cwd: this.config.cwd ?? null,
approvalPolicy,
sandboxPolicyType,
hasCollaborationMode: Boolean(this.resolvedCollaborationMode),
hasOutputSchema,
hasDeveloperInstructions: Boolean(this.config.systemPrompt?.trim()),
hasCodexConfig,
},
"Starting Codex app-server turn",
);
}
async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> {
return runProviderTurn({
prompt,
runOptions: options,
startTurn: (p, o) => this.startTurn(p, o),
subscribe: (callback) => this.subscribe(callback),
getSessionId: async () => (await this.getRuntimeInfo()).sessionId ?? "",
reduceFinalText: ({ current, item }) => {
if (item.type === "assistant_message") {
return item.text;
}
if (item.type === "tool_call" && item.detail.type === "plan") {
return item.detail.text;
}
return current;
},
});
}
async startTurn(
prompt: AgentPromptInput,
options?: AgentRunOptions,
): Promise<{ turnId: string }> {
if (this.activeForegroundTurnId) {
throw new Error("A foreground turn is already active");
}
await this.connect();
if (!this.client) {
throw new Error("Codex client not initialized");
}
const slashCommand = await this.resolveSlashCommandInvocation(prompt);
const effectivePrompt = slashCommand
? await this.buildCommandPromptInput(slashCommand.commandName, slashCommand.args)
: prompt;
if (this.currentThreadId) {
await this.ensureThreadLoaded();
} else {
await this.ensureThread();
}
const turnStart = await this.buildTurnStartParams(effectivePrompt, options);
const turnId = this.createTurnId();
this.activeForegroundTurnId = turnId;
try {
await this.client.request("turn/start", params, TURN_START_TIMEOUT_MS);
this.logTurnStartSummary({
turnId,
thinkingOptionId: turnStart.thinkingOptionId,
approvalPolicy: turnStart.approvalPolicy,
sandboxPolicyType: turnStart.sandboxPolicyType,
hasOutputSchema: turnStart.hasOutputSchema,
hasCodexConfig: turnStart.hasCodexConfig,
});
await this.client.request("turn/start", turnStart.params, TURN_START_TIMEOUT_MS);
} catch (error) {
this.activeForegroundTurnId = null;
throw error;
@@ -3288,6 +3354,11 @@ class CodexAppServerAgentSession implements AgentSession {
async setFeature(featureId: string, value: unknown): Promise<void> {
if (featureId === "fast_mode") {
if (Boolean(value) && !codexModelSupportsFastMode(this.config.model)) {
throw new Error(
`Codex fast mode is not available for model '${this.config.model ?? "default"}'`,
);
}
this.applyFeatureValue("fast_mode", Boolean(value));
return;
}

View File

@@ -6,6 +6,7 @@ import path from "node:path";
import type {
AgentCapabilityFlags,
AgentClient,
AgentFeature,
AgentLaunchContext,
AgentMode,
AgentModelDefinition,
@@ -32,6 +33,8 @@ const TEST_CAPABILITIES: AgentCapabilityFlags = {
supportsToolInvocations: true,
};
const TEST_FEATURE_ID = "test_feature";
interface Deferred<T> {
promise: Promise<T>;
resolve: (value: T) => void;
@@ -325,6 +328,18 @@ class FakeAgentSession implements AgentSession {
return this.providerName;
}
get features(): AgentFeature[] {
return [
{
type: "toggle",
id: TEST_FEATURE_ID,
label: "Test feature",
description: "Deterministic provider feature used by MCP integration tests.",
value: this.config.featureValues?.[TEST_FEATURE_ID] === true,
},
];
}
private async appendHistoryEvent(event: AgentStreamEvent): Promise<void> {
const folder = path.dirname(this.historyPath);
await mkdir(folder, { recursive: true });
@@ -786,6 +801,13 @@ class FakeAgentSession implements AgentSession {
this.config.modeId = modeId;
}
async setFeature(featureId: string, value: unknown): Promise<void> {
this.config.featureValues = {
...this.config.featureValues,
[featureId]: value,
};
}
getPendingPermissions(): AgentPermissionRequest[] {
return this.pendingPermissions;
}

View File

@@ -15,19 +15,20 @@ The MCP server itself is controlled by `daemon.mcp.enabled`. Existing agents may
### Agents
| Tool | Function |
| -------------------- | ----------------------------------------------------------------------------------------------------- |
| `create_agent` | Create an agent tied to a working directory, optionally with an initial prompt or a new git worktree. |
| `wait_for_agent` | Block until an agent requests permission or finishes its current run. |
| `send_agent_prompt` | Send a task to a running agent. |
| `get_agent_status` | Return the latest snapshot for an agent. |
| `list_agents` | List recent agents as compact metadata. |
| `cancel_agent` | Abort an agent's current run but keep the agent alive. |
| `archive_agent` | Soft-delete an agent and remove it from the active list. |
| `kill_agent` | Terminate an agent session permanently. |
| `update_agent` | Update an agent name or labels. |
| `get_agent_activity` | Return recent agent timeline entries as a curated summary. |
| `set_agent_mode` | Switch an agent's session mode. |
| Tool | Function |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `create_agent` | Create an agent tied to a working directory, optionally with an initial prompt, provider features, or a new git worktree. |
| `wait_for_agent` | Block until an agent requests permission or finishes its current run. |
| `send_agent_prompt` | Send a task to a running agent. |
| `get_agent_status` | Return the latest snapshot for an agent. |
| `list_agents` | List recent agents as compact metadata. |
| `cancel_agent` | Abort an agent's current run but keep the agent alive. |
| `archive_agent` | Soft-delete an agent and remove it from the active list. |
| `kill_agent` | Terminate an agent session permanently. |
| `update_agent` | Update an agent name or labels. |
| `get_agent_activity` | Return recent agent timeline entries as a curated summary. |
| `set_agent_mode` | Switch an agent's session mode. |
| `set_agent_feature` | Set a provider-specific feature on an existing agent, for example Codex `fast_mode`. |
### Terminals

View File

@@ -20,12 +20,16 @@ Returns `{ branchName, worktreePath }`. Pass `cwd` to target a specific repo.
## Agents
**`create_agent`** — required: `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Common: `cwd` (often a `worktreePath`), `background` (default `false` — blocks until completion or permission), `notifyOnFinish`. Returns `{ agentId, … }`.
**`create_agent`** — required: `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Common: `cwd` (often a `worktreePath`), `background` (default `false` — blocks until completion or permission), `notifyOnFinish`, `features`. Returns `{ agentId, … }`.
Provider features are provider-specific. For Codex fast mode, pass `features: { "fast_mode": true }` when creating the agent.
Compose: call `create_worktree` first, then `create_agent` with `cwd` set to the returned `worktreePath`.
**`send_agent_prompt`** — `{ agentId, prompt }`. Blocks by default; pass `background: true` to fire-and-forget.
**`set_agent_feature`** — `{ agentId, featureId, value }`. Use for provider-specific toggles on an existing agent, for example `{ agentId, featureId: "fast_mode", value: true }` for Codex.
**`list_agents`** — filter by `cwd`, `statuses`, `sinceHours`, `includeArchived`.
**`archive_agent`** — `{ agentId }`. Interrupts if running, removes from active list.