From bd5e87898ae59fb7a8a9048634dbd2880b28933d Mon Sep 17 00:00:00 2001 From: SSU-WEI HUANG Date: Sun, 26 Jul 2026 15:01:53 +0800 Subject: [PATCH] feat(codex): support proactive /agent mode (#1172) --- cli/src/codex/loop.ts | 1 + cli/src/codex/runCodex.ts | 11 ++++++++- cli/src/codex/utils/appServerConfig.test.ts | 17 +++++++++++++ cli/src/codex/utils/appServerConfig.ts | 12 ++++++--- cli/src/codex/utils/slashCommands.test.ts | 13 ++++++++++ cli/src/codex/utils/slashCommands.ts | 27 +++++++++++++++++++++ shared/src/slashCommands.ts | 1 + 7 files changed, 78 insertions(+), 4 deletions(-) diff --git a/cli/src/codex/loop.ts b/cli/src/codex/loop.ts index b05a3fed..a7c489f1 100644 --- a/cli/src/codex/loop.ts +++ b/cli/src/codex/loop.ts @@ -15,6 +15,7 @@ export interface EnhancedMode { permissionMode: PermissionMode; model?: string; collaborationMode: CodexCollaborationMode; + proactiveMultiAgent?: boolean; modelReasoningEffort?: ReasoningEffort; /** * Service tier override. `undefined` leaves it untouched (account default), diff --git a/cli/src/codex/runCodex.ts b/cli/src/codex/runCodex.ts index fa133e24..4534336c 100644 --- a/cli/src/codex/runCodex.ts +++ b/cli/src/codex/runCodex.ts @@ -73,6 +73,7 @@ export async function runCodex(opts: { model: mode.model, modelReasoningEffort: mode.modelReasoningEffort, collaborationMode: mode.collaborationMode, + proactiveMultiAgent: mode.proactiveMultiAgent, serviceTier: mode.serviceTier })); @@ -89,6 +90,7 @@ export async function runCodex(opts: { let currentModel = opts.model; let currentModelReasoningEffort: ReasoningEffort | undefined = opts.modelReasoningEffort; let currentCollaborationMode: EnhancedMode['collaborationMode'] = opts.collaborationMode ?? 'default'; + let currentProactiveMultiAgent: boolean | undefined; // Service tier (Fast mode), stored representation: `'fast'` and // `'standard'` are explicit user choices, `undefined`/`null` mean untouched // (use the account default). Prefer the spawn-time override (set by the hub @@ -137,6 +139,7 @@ export async function runCodex(opts: { modelReasoningEffort?: ReasoningEffort | null; collaborationMode?: EnhancedMode['collaborationMode']; serviceTier?: string | null; + proactiveMultiAgent?: boolean; } | undefined): void => { if (!updates) return; if (updates.permissionMode !== undefined) { @@ -154,6 +157,9 @@ export async function runCodex(opts: { if (updates.serviceTier !== undefined) { currentServiceTier = updates.serviceTier; } + if (updates.proactiveMultiAgent !== undefined) { + currentProactiveMultiAgent = updates.proactiveMultiAgent; + } applyCurrentConfigToSession(); }; @@ -194,7 +200,8 @@ export async function runCodex(opts: { collaborationMode: currentCollaborationMode, model: currentModel, modelReasoningEffort: currentModelReasoningEffort, - serviceTier: currentServiceTier + serviceTier: currentServiceTier, + proactiveMultiAgent: currentProactiveMultiAgent }); if (slash.kind === 'goal') { if (slash.message) { @@ -253,6 +260,7 @@ export async function runCodex(opts: { model: currentModel, modelReasoningEffort: currentModelReasoningEffort, collaborationMode: currentCollaborationMode, + proactiveMultiAgent: currentProactiveMultiAgent, serviceTier: currentServiceTier }; if (isolatedCommandText) { @@ -267,6 +275,7 @@ export async function runCodex(opts: { model: currentModel, modelReasoningEffort: currentModelReasoningEffort, collaborationMode: currentCollaborationMode, + proactiveMultiAgent: currentProactiveMultiAgent, serviceTier: currentServiceTier }; messageQueue.push(formatMessageWithAttachments(message.content.text, message.content.attachments), enhancedMode, localId); diff --git a/cli/src/codex/utils/appServerConfig.test.ts b/cli/src/codex/utils/appServerConfig.test.ts index 795810e8..09ddef2c 100644 --- a/cli/src/codex/utils/appServerConfig.test.ts +++ b/cli/src/codex/utils/appServerConfig.test.ts @@ -422,6 +422,23 @@ describe('appServerConfig', () => { expect(instructions).toContain('Do not rely on parent turn reasoning settings for spawned agents'); }); + it('injects proactive multi-agent instructions when /agent mode is enabled', () => { + const params = buildTurnStartParams({ + threadId: 'thread-1', + message: 'work', + cwd: '/repo', + mode: { + permissionMode: 'default', + model: 'o3', + collaborationMode: 'default', + proactiveMultiAgent: true + } + }); + + expect(params.collaborationMode?.settings.developer_instructions) + .toContain('Proactive multi-agent delegation is active.'); + }); + it('rejects collaboration mode payloads without a resolved model', () => { expect(() => buildTurnStartParams({ threadId: 'thread-1', diff --git a/cli/src/codex/utils/appServerConfig.ts b/cli/src/codex/utils/appServerConfig.ts index f8e10c7c..bd2591b9 100644 --- a/cli/src/codex/utils/appServerConfig.ts +++ b/cli/src/codex/utils/appServerConfig.ts @@ -124,8 +124,14 @@ function resolveInstructions(args: { }; } -function appendCollaborationInstructions(developerInstructions: string): string { - return `${developerInstructions}\n\n${codexCollaborationSpawnAgentInstructions}`; +function appendCollaborationInstructions(developerInstructions: string, proactiveMultiAgent?: boolean): string { + if (proactiveMultiAgent === undefined) { + return `${developerInstructions}\n\n${codexCollaborationSpawnAgentInstructions}`; + } + const multiAgentMode = proactiveMultiAgent + ? 'Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Use sub-agents when parallel work would materially improve speed or quality. This mode remains active until a later multi-agent mode developer message changes it.' + : 'Any earlier instruction enabling proactive multi-agent delegation no longer applies. Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work.'; + return `${developerInstructions}\n\n${codexCollaborationSpawnAgentInstructions}\n\n${multiAgentMode}`; } function mentionNameFromPath(path: string): string { @@ -275,7 +281,7 @@ export function buildTurnStartParams(args: { settings: { model, reasoning_effort: modelReasoningEffort ?? null, - developer_instructions: appendCollaborationInstructions(developerInstructions) + developer_instructions: appendCollaborationInstructions(developerInstructions, args.mode?.proactiveMultiAgent) } }; } else if (model) { diff --git a/cli/src/codex/utils/slashCommands.test.ts b/cli/src/codex/utils/slashCommands.test.ts index c07fc5e1..67e8a0bc 100644 --- a/cli/src/codex/utils/slashCommands.test.ts +++ b/cli/src/codex/utils/slashCommands.test.ts @@ -9,6 +9,19 @@ const state = { }; describe('resolveCodexSlashCommand', () => { + it('toggles proactive multi-agent mode', () => { + expect(resolveCodexSlashCommand('/agent', state)).toMatchObject({ + updates: { proactiveMultiAgent: true } + }); + expect(resolveCodexSlashCommand('/agent off', { ...state, proactiveMultiAgent: true })).toMatchObject({ + updates: { proactiveMultiAgent: false } + }); + expect(resolveCodexSlashCommand('/agent status', { ...state, proactiveMultiAgent: true })).toEqual({ + kind: 'handled', + message: 'Codex proactive multi-agent mode: on' + }); + }); + it('enables plan mode without sending a turn', () => { expect(resolveCodexSlashCommand('/plan', state)).toEqual({ kind: 'handled', diff --git a/cli/src/codex/utils/slashCommands.ts b/cli/src/codex/utils/slashCommands.ts index 7c481fe1..6a206cd6 100644 --- a/cli/src/codex/utils/slashCommands.ts +++ b/cli/src/codex/utils/slashCommands.ts @@ -33,6 +33,7 @@ export type CodexSlashResolution = model?: string | null; modelReasoningEffort?: ReasoningEffort | null; serviceTier?: string | null; + proactiveMultiAgent?: boolean; }; } | { @@ -45,6 +46,7 @@ export type CodexSlashResolution = model?: string | null; modelReasoningEffort?: ReasoningEffort | null; serviceTier?: string | null; + proactiveMultiAgent?: boolean; }; } | { @@ -63,6 +65,7 @@ export function resolveCodexSlashCommand( model?: string; modelReasoningEffort?: ReasoningEffort; serviceTier?: string | null; + proactiveMultiAgent?: boolean; } ): CodexSlashResolution { const match = /^\s*\/([a-z0-9:_-]+)(?:\s+([\s\S]*))?$/i.exec(text); @@ -107,6 +110,30 @@ export function resolveCodexSlashCommand( }; } + if (command === 'agent') { + const value = rest.toLowerCase(); + if (value === 'status') { + return { + kind: 'handled', + message: `Codex proactive multi-agent mode: ${state.proactiveMultiAgent ? 'on' : 'off'}` + }; + } + if (value && !['on', 'enable', 'enabled', 'off', 'disable', 'disabled'].includes(value)) { + return { + kind: 'handled', + message: 'Usage: /agent [on|off|status]' + }; + } + const enabled = value + ? ['on', 'enable', 'enabled'].includes(value) + : !state.proactiveMultiAgent; + return { + kind: 'handled', + message: `Codex proactive multi-agent mode ${enabled ? 'enabled' : 'disabled'}`, + updates: { proactiveMultiAgent: enabled } + }; + } + if (command === 'goal') { const lowerRest = rest.toLowerCase(); if (!rest) { diff --git a/shared/src/slashCommands.ts b/shared/src/slashCommands.ts index 33ff4a30..dbc5b74c 100644 --- a/shared/src/slashCommands.ts +++ b/shared/src/slashCommands.ts @@ -12,6 +12,7 @@ export const BUILTIN_SLASH_COMMANDS = { { name: 'status', description: 'Show Claude Code status including version, model, account, and API connectivity', source: 'builtin' }, ], codex: [ + { name: 'agent', description: 'Toggle proactive Codex multi-agent delegation', source: 'builtin' }, { name: 'clear', description: 'Clear current Codex thread context', source: 'builtin' }, { name: 'compact', description: 'Compact current Codex thread context', source: 'builtin' }, { name: 'goal', description: 'Set, view, pause, resume, or clear a persistent Codex goal', source: 'builtin' },