diff --git a/docs-site/src/content/docs/ja/reference/configuration/agents.md b/docs-site/src/content/docs/ja/reference/configuration/agents.md index 2b185b81c4..1c700bc898 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ja/reference/configuration/agents.md @@ -26,6 +26,14 @@ description: マルチエージェント サーフェス、委任ガイダンス 管理 API は、`GET`/`PUT /api/v2`、`/api/injection-model`、`/api/effort-caps`、`/api/subagent-models`、および `/api/subagent-model-fallback` を公開します。インジェクションモデルの更新は部分的です。カスタム プロンプトは、その API の `prompt` フィールドです。 +## 常時プロアクティブ委任 + +Subagents → 詳細設定の **常時プロアクティブ委任**(旧 **Ultra mode**)は、推論のエフォートを変更せず、委任を開始する条件だけを変更します。推奨プリセットでも、ユーザーの指示、権限の境界、タスクの範囲、ツールの規則は維持されます。 + +`GET` と `PUT /api/v2` は、追加で `multiAgentModeHintRecommendation: { text, revision }` を返します。ダッシュボードはプリセットの有効化と復元にサーバー提供のテキストを使い、固定文へのフォールバックは行いません。旧サーバーが推奨値を返さない場合や値の形式が不正な場合、プリセットの適用と復元は利用できませんが、既存のカスタムヒントの編集と削除は引き続き利用できます。プリセットの復元はローカルの下書きだけを変更し、保存操作で確定します。 + +設定の読み取り、無関係な更新、アップグレードでは、保存済みのヒントは移行されません。ヒントを明示的に更新し、その本文が既知の旧 OpenCodex プリセット2種類のいずれかとバイト単位で完全一致する場合に限り、現在の推奨文に置き換わります。それ以外の有効なカスタムテキストは、空白だけが異なるものも含め、バイト単位で保持されます。既存の v2 有効化、機能サポート確認、ヒント削除の規則は変わらず、変更は新しい Codex セッションに適用されます。 + ## ロスターとガイダンス 有効な v2 ロスターは、v2 と互換性があり、挿入されたカタログに存在する、構成され、ピッカーに表示され、優先順位で並べ替えられた最初の 5 つのモデルです。 V2 の適格性は、明示的な `"v2"`、`null`、または欠落しているアップストリーム ピンを適格なものとして扱います。実際の `"v1"` ピンは除外されます。除外されたエントリは設定に残るため、後で適格になる可能性があります。 diff --git a/docs-site/src/content/docs/ko/reference/configuration/agents.md b/docs-site/src/content/docs/ko/reference/configuration/agents.md index 1c999536f2..f5903e221e 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ko/reference/configuration/agents.md @@ -26,6 +26,14 @@ description: 멀티 에이전트 표면, 위임 안내, 선호 모델, 대체 관리 API는 `GET`/`PUT /api/v2`, `/api/injection-model`, `/api/effort-caps`, `/api/subagent-models`, `/api/subagent-model-fallback`를 제공합니다. injection-model 업데이트는 부분 업데이트입니다. 사용자 지정 프롬프트는 이 API의 `prompt` 필드입니다. +## 항상 능동 위임 + +서브에이전트 → 고급의 **항상 능동 위임**(이전 이름: **울트라 모드**)은 추론 노력을 바꾸지 않고 위임을 시작하는 조건만 변경합니다. 권장 프리셋에서도 사용자 지침, 권한 경계, 작업 범위, 도구 규칙은 유지됩니다. + +`GET`과 `PUT /api/v2`는 `multiAgentModeHintRecommendation: { text, revision }`도 반환합니다. 대시보드는 프리셋을 켜거나 복원할 때 서버가 제공한 텍스트를 사용하며, 고정된 대체 문구는 사용하지 않습니다. 이전 서버가 권장값을 제공하지 않거나 잘못된 형식으로 반환하면 프리셋 적용·복원은 사용할 수 없으며, 기존 사용자 지정 힌트의 편집·삭제는 계속 사용할 수 있습니다. **프리셋 복원**은 로컬 초안만 바꾸고, **저장**을 눌러야 저장됩니다. + +설정 조회, 관련 없는 업데이트, 업그레이드는 저장된 힌트를 자동 변경하지 않습니다. 힌트를 명시적으로 업데이트할 때 본문이 알려진 두 가지 이전 OpenCodex 프리셋 중 하나와 바이트 단위로 정확히 일치하는 경우에만 현재 권장 문구로 바뀝니다. 그 밖의 유효한 사용자 지정 텍스트는 공백 차이가 있는 변형까지 바이트 단위로 보존됩니다. 기존 v2 활성화·기능 지원 확인·힌트 해제 규칙은 그대로이며, 변경은 새 Codex 세션에 적용됩니다. + ## 로스터와 안내 실제 v2 로스터는 설정되어 있고, 선택기에 보이며, 우선순위로 정렬된 상위 다섯 모델 중 v2와 호환되고 주입된 카탈로그에 존재하는 모델입니다. v2 적격성은 명시적인 `"v2"`, `null`, 또는 생략된 상위 고정값을 적격으로 보고, 실제 `"v1"` 고정값은 제외합니다. 제외된 항목은 나중에 적격이 될 수 있도록 설정에 그대로 남습니다. diff --git a/docs-site/src/content/docs/reference/configuration/agents.md b/docs-site/src/content/docs/reference/configuration/agents.md index f3caea02f2..7a154e2b5c 100644 --- a/docs-site/src/content/docs/reference/configuration/agents.md +++ b/docs-site/src/content/docs/reference/configuration/agents.md @@ -45,20 +45,33 @@ Mode changes apply to new sessions. `maxConcurrentThreadsPerSession` is a `PUT / `config.json` key; `ocx v2 threads ` writes `max_concurrent_threads_per_session` under `[features.multi_agent_v2]` in Codex's `$CODEX_HOME/config.toml` after v2 is enabled. -**Ultra mode** (the Subagents dashboard toggle, `PUT /api/v2` field -`multiAgentModeHintText`, and `ocx v2 mode-hint`) writes +**Always proactive delegation** in Subagents → Advanced (formerly **Ultra mode**) changes the +delegation trigger without changing reasoning effort. Its preset preserves user instructions, +authority boundaries, task scope, and tool rules. The dashboard toggle, `PUT /api/v2` field +`multiAgentModeHintText`, and `ocx v2 mode-hint` write `features.multi_agent_v2.multi_agent_mode_hint_text` in Codex's `$CODEX_HOME/config.toml`. The CLI `ocx v2 mode-hint` command persists this key even -when `multi_agent_v2` is disabled; it does not toggle the feature. The hint overrides -codex-rs's effort-derived multi-agent policy, so any model and any reasoning effort -receives the Proactive delegation prompt; it does **not** change reasoning effort. +when `multi_agent_v2` is disabled; it does not toggle the feature. The hint replaces +codex-rs's effort-derived multi-agent policy when that native surface is active. A `null` value removes the key so the effort-derived policy (ultra = proactive, otherwise explicit) resumes; empty or whitespace-only values are rejected because a present empty override would suppress even the ultra-derived Proactive message. The -Subagents dashboard's Ultra mode **on** toggle requires both the native feature and +Subagents dashboard's **Always proactive delegation** toggle requires both the native feature and an explicit v2 surface (`multiAgentMode: "v2"`, equivalent to `ocx v2 mode v2`); `ocx v2 on` alone does not satisfy that dashboard gate. +`GET` and `PUT /api/v2` also return `multiAgentModeHintRecommendation: { text, revision }`. +The dashboard uses this server-provided text when enabling or restoring the preset, with no +hardcoded fallback. If an older server omits the recommendation or returns a malformed value, +preset installation and restoration are unavailable; editing or clearing an existing custom hint +remains available. **Restore preset** changes only the local draft; **Save** persists it. + +Reading settings, unrelated updates, and upgrades do not migrate a stored hint. Only an explicit +hint update that matches either of the two recognized legacy OpenCodex presets byte-for-byte is +replaced with the current recommendation. Other valid custom text, including whitespace variants, +is preserved byte-for-byte. Mode-hint support is still checked before writing, and changes apply +to new Codex sessions. + The management API exposes `GET`/`PUT /api/v2`, `/api/injection-model`, `/api/effort-caps`, `/api/subagent-models`, and `/api/subagent-model-fallback`. Injection-model updates are partial; the custom prompt is the `prompt` field on that API. diff --git a/docs-site/src/content/docs/ru/reference/configuration/agents.md b/docs-site/src/content/docs/ru/reference/configuration/agents.md index a3a0bc434b..e932a0113b 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ru/reference/configuration/agents.md @@ -33,6 +33,14 @@ Management API предоставляет `GET`/`PUT /api/v2`, `/api/injection-m `/api/subagent-models` и `/api/subagent-model-fallback`. Обновления injection-model частичные; custom prompt на этом API передаётся полем `prompt`. +## Всегда проактивное делегирование + +**Всегда проактивное делегирование** в Subagents → Дополнительно (прежнее название — **Ultra mode**) меняет только условие запуска делегирования, не меняя reasoning effort. Рекомендуемый preset сохраняет инструкции пользователя, границы полномочий, область задачи и правила работы с инструментами. + +`GET` и `PUT /api/v2` дополнительно возвращают `multiAgentModeHintRecommendation: { text, revision }`. При включении или восстановлении preset дашборд использует текст сервера без встроенного запасного варианта. Если старый сервер не возвращает рекомендацию или возвращает некорректное значение, применение и восстановление preset недоступны; существующий custom hint по-прежнему можно редактировать или удалять. Восстановление preset меняет только локальный черновик; действие сохранения записывает его. + +Чтение настроек, несвязанные изменения и обновление версии не мигрируют сохранённый hint. Только явное обновление hint, текст которого побайтово совпадает с одним из двух известных старых preset OpenCodex, заменяет его текущей рекомендацией. Остальной допустимый custom text, включая варианты с отличиями в пробельных символах, сохраняется побайтово. Существующие правила включения v2, проверки поддержки и удаления hint не меняются; изменения применяются к новым сессиям Codex. + ## Roster и guidance Эффективный ростер v2 — это настроенные, видимые в picker'е, отсортированные по priority первые diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md index 1c8d71eeca..6ec078839e 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md @@ -26,6 +26,14 @@ description: 多代理界面、委派引导、首选模型、回退链、原生 管理 API 公开 `GET`/`PUT /api/v2`、`/api/injection-model`、`/api/effort-caps`、`/api/subagent-models` 和 `/api/subagent-model-fallback`。injection-model 更新是部分更新;自定义 prompt 是该 API 上的 `prompt` 字段。 +## 始终主动委派 + +Subagents → 高级中的 **始终主动委派**(原名 **Ultra mode**)只改变触发委派的条件,不改变推理 effort。推荐预设仍遵循用户指令、权限边界、任务范围和工具规则。 + +`GET` 和 `PUT /api/v2` 还会返回 `multiAgentModeHintRecommendation: { text, revision }`。仪表板在启用或恢复预设时使用服务器提供的文本,不会回退到硬编码文案。如果旧服务器没有提供推荐值,或返回的值格式无效,则无法应用或恢复预设;仍可编辑或清除现有的自定义提示。恢复预设只修改本地草稿,保存操作才会将其写入配置。 + +读取设置、无关更新和版本升级不会迁移已保存的提示。只有显式更新提示,且正文与两种已知旧版 OpenCodex 预设之一逐字节完全一致时,才会替换为当前推荐文本。其他有效的自定义文本,包括仅空白字符不同的变体,都会逐字节保留。现有的 v2 启用、功能支持检查和清除提示规则保持不变;更改会应用于新的 Codex 会话。 + ## 名单与引导 有效的 v2 名单,是已配置、在选择器中可见、按优先级排序的前五个模型中,和 v2 兼容且存在于注入目录中的那些模型。v2 资格判定会把显式的 `"v2"`、`null`,或缺失的上游固定值视为可用;真正的 `"v1"` 固定值会被排除。被排除的条目仍会保留在配置中,以便将来重新变为可用。 diff --git a/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx b/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx index a975786a94..19108e04cd 100644 --- a/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx +++ b/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx @@ -299,10 +299,15 @@ export default function SubagentDelegationSection({ @@ -385,7 +390,3 @@ function UltraModeEditor({ ); } - -/** Canonical Proactive delegation text mirrored from codex-rs (multi_agent_mode_instructions.rs). */ -export const ULTRA_MODE_PRESET = - "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently. Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself. This mode remains active until a later multi-agent mode developer message changes it."; diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index b7e62e0ea8..40ee6ea36f 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -680,14 +680,14 @@ export const de: Record = { "sub.workspace.selectModel": "Modell auswählen", "sub.workspace.selectModelDesc": "Wählen Sie ein Modell aus der Liste, um Details anzuzeigen und es für spawn_agent hervorzuheben.", "sub.workspace.selector": "Öffentlicher Selektor", - "sub.ultraMode": "Ultra-Modus", + "sub.ultraMode": "Immer proaktiv delegieren", "sub.ultraModeHint": "Aktiviert die proaktive Multi-Agent-Delegierungsrichtlinie für alle Modelle und Reasoning-Efforts (ändert den Reasoning-Effort selbst nicht). Schreibt features.multi_agent_v2.multi_agent_mode_hint_text in config.toml.", "sub.ultraModeV2Required": "Erfordert die v2-Multi-Agent-Oberfläche — aktivieren Sie zuerst multi_agent_v2 und wählen Sie v2 in der Subagentenmodus-Steuerung.", - "sub.ultraModeText": "Delegierungstext des Ultra-Modus", + "sub.ultraModeText": "Text für proaktive Delegierung", "sub.ultraModePreset": "Voreinstellung wiederherstellen", - "sub.ultraModeLoadFail": "Ultra-Modus-Einstellungen konnten nicht geladen werden — läuft der Proxy?", - "sub.ultraModeSaveFail": "Ultra-Modus-Einstellungen konnten nicht gespeichert werden", - "sub.ultraModeSaved": "Ultra-Modus gespeichert. Gilt für neue Codex-Sitzungen.", + "sub.ultraModeLoadFail": "Einstellungen für proaktive Delegierung konnten nicht geladen werden — läuft der Proxy?", + "sub.ultraModeSaveFail": "Einstellungen für proaktive Delegierung konnten nicht gespeichert werden", + "sub.ultraModeSaved": "Proaktive Delegierung gespeichert. Gilt für neue Codex-Sitzungen.", "logs.title": "Anfrage-Protokolle", "logs.tabLogs": "Protokolle", "logs.tabDebug": "Diagnose", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index fdb91fad32..53d940d7e6 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -709,14 +709,14 @@ export const en = { "sub.workspace.mainAria": "Subagent model details", "sub.workspace.notFeatured": "Not featured", "sub.workspace.priority": "Priority", - "sub.ultraMode": "Ultra mode", + "sub.ultraMode": "Always proactive delegation", "sub.ultraModeHint": "Enable the Proactive multi-agent delegation policy for every model and reasoning effort (does not change reasoning effort itself). Writes features.multi_agent_v2.multi_agent_mode_hint_text in config.toml.", "sub.ultraModeV2Required": "Requires the v2 multi-agent surface — enable multi_agent_v2 and select v2 in the Sub-agent mode control first.", - "sub.ultraModeText": "Ultra mode delegation text", + "sub.ultraModeText": "Proactive delegation text", "sub.ultraModePreset": "Restore preset", - "sub.ultraModeLoadFail": "Failed to load Ultra mode settings — is the proxy running?", - "sub.ultraModeSaveFail": "Failed to save Ultra mode settings", - "sub.ultraModeSaved": "Ultra mode saved. Applies to new Codex sessions.", + "sub.ultraModeLoadFail": "Failed to load proactive delegation settings — is the proxy running?", + "sub.ultraModeSaveFail": "Failed to save proactive delegation settings", + "sub.ultraModeSaved": "Proactive delegation saved. Applies to new Codex sessions.", "sub.workspace.removeFromFeatured": "Remove {m} from featured", "sub.workspace.selectModel": "Select a model", "sub.workspace.selectModelDesc": "Pick a model from the list to see details and feature it for spawn_agent.", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 4354e6fd56..7afb44ab2c 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -692,14 +692,14 @@ export const fr: Record = { "sub.workspace.mainAria": "Détails du modèle de sous-agent", "sub.workspace.notFeatured": "Non mis à la une", "sub.workspace.priority": "Priorité", - "sub.ultraMode": "Mode Ultra", + "sub.ultraMode": "Délégation toujours proactive", "sub.ultraModeHint": "Activer la politique de délégation multi-agent proactive pour tous les modèles et niveaux de raisonnement (sans modifier le niveau de raisonnement lui-même). Écrit features.multi_agent_v2.multi_agent_mode_hint_text dans config.toml.", "sub.ultraModeV2Required": "Nécessite l’interface multi-agent v2 — activez multi_agent_v2 et sélectionnez d’abord v2 dans le contrôle du mode Sous-agent.", - "sub.ultraModeText": "Texte de délégation du mode Ultra", + "sub.ultraModeText": "Texte de délégation proactive", "sub.ultraModePreset": "Rétablir le préréglage", - "sub.ultraModeLoadFail": "Échec du chargement des paramètres du mode Ultra — le proxy est-il en cours d’exécution ?", - "sub.ultraModeSaveFail": "Échec de l’enregistrement des paramètres du mode Ultra", - "sub.ultraModeSaved": "Mode Ultra enregistré. S’applique aux nouvelles sessions Codex.", + "sub.ultraModeLoadFail": "Échec du chargement des paramètres de délégation proactive — le proxy est-il en cours d’exécution ?", + "sub.ultraModeSaveFail": "Échec de l’enregistrement des paramètres de délégation proactive", + "sub.ultraModeSaved": "Délégation proactive enregistrée. S’applique aux nouvelles sessions Codex.", "sub.workspace.removeFromFeatured": "Retirer {m} de la sélection À la une", "sub.workspace.selectModel": "Sélectionner un modèle", "sub.workspace.selectModelDesc": "Choisissez un modèle dans la liste pour afficher ses détails et le mettre à la une pour spawn_agent.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index dfbab83390..75095da332 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -640,14 +640,14 @@ export const ja: Record = { "sub.workspace.selectModel": "モデルを選択", "sub.workspace.selectModelDesc": "一覧からモデルを選んで詳細を確認し、spawn_agent のおすすめに設定します。", "sub.workspace.selector": "公開セレクター", - "sub.ultraMode": "ウルトラモード", + "sub.ultraMode": "常時プロアクティブ委任", "sub.ultraModeHint": "すべてのモデルと reasoning effort で Proactive マルチエージェント委任ポリシーを有効にします(reasoning effort 自体は変更しません)。config.toml に features.multi_agent_v2.multi_agent_mode_hint_text を書き込みます。", "sub.ultraModeV2Required": "v2 マルチエージェントサーフェスが必要です — 先に multi_agent_v2 を有効にし、サブエージェントモードで v2 を選択してください。", - "sub.ultraModeText": "ウルトラモード委任テキスト", + "sub.ultraModeText": "プロアクティブ委任テキスト", "sub.ultraModePreset": "プリセットを復元", - "sub.ultraModeLoadFail": "ウルトラモード設定を読み込めませんでした — プロキシは実行中ですか?", - "sub.ultraModeSaveFail": "ウルトラモード設定の保存に失敗しました", - "sub.ultraModeSaved": "ウルトラモードを保存しました。新しい Codex セッションから適用されます。", + "sub.ultraModeLoadFail": "プロアクティブ委任設定を読み込めませんでした — プロキシは実行中ですか?", + "sub.ultraModeSaveFail": "プロアクティブ委任設定の保存に失敗しました", + "sub.ultraModeSaved": "プロアクティブ委任設定を保存しました。新しい Codex セッションから適用されます。", // logs "logs.title": "リクエストログ", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 19855bdd33..5ae812e9e8 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -697,14 +697,14 @@ export const ko: Record = { "sub.workspace.selectModel": "모델 선택", "sub.workspace.selectModelDesc": "목록에서 모델을 선택하여 세부 정보를 확인하고 spawn_agent에 추천하세요.", "sub.workspace.selector": "공개 셀렉터", - "sub.ultraMode": "울트라 모드", + "sub.ultraMode": "항상 능동 위임", "sub.ultraModeHint": "모든 모델과 reasoning effort에서 Proactive 멀티에이전트 위임 정책을 켭니다 (reasoning effort 자체는 변경하지 않음). config.toml에 features.multi_agent_v2.multi_agent_mode_hint_text를 기록합니다.", "sub.ultraModeV2Required": "v2 멀티에이전트 서피스가 필요합니다 — 먼저 multi_agent_v2를 켜고 서브에이전트 모드에서 v2를 선택하세요.", - "sub.ultraModeText": "울트라 모드 위임 텍스트", + "sub.ultraModeText": "능동 위임 텍스트", "sub.ultraModePreset": "프리셋 복원", - "sub.ultraModeLoadFail": "울트라 모드 설정을 불러오지 못했습니다 — 프록시가 실행 중인가요?", - "sub.ultraModeSaveFail": "울트라 모드 설정 저장에 실패했습니다", - "sub.ultraModeSaved": "울트라 모드가 저장되었습니다. 새 Codex 세션부터 적용됩니다.", + "sub.ultraModeLoadFail": "능동 위임 설정을 불러오지 못했습니다 — 프록시가 실행 중인가요?", + "sub.ultraModeSaveFail": "능동 위임 설정 저장에 실패했습니다", + "sub.ultraModeSaved": "능동 위임 설정이 저장되었습니다. 새 Codex 세션부터 적용됩니다.", "sub.fallbackLabel": "서브에이전트 폴백 체인", "sub.fallbackHint": "서브에이전트 모델을 사용할 수 없거나 실패할 때 순서대로 시도할 모델입니다.", "sub.fallbackAdd": "폴백 모델 추가…", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 194d7aa72a..2900abb6f3 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -695,14 +695,14 @@ export const ru: Record = { "sub.workspace.selectModel": "Выберите модель", "sub.workspace.selectModelDesc": "Выберите модель из списка, чтобы увидеть детали и добавить её в избранные для spawn_agent.", "sub.workspace.selector": "Публичный селектор", - "sub.ultraMode": "Ультра-режим", + "sub.ultraMode": "Всегда проактивное делегирование", "sub.ultraModeHint": "Включает политику упреждающего делегирования мультиагентов для всех моделей и уровней reasoning effort (сам reasoning effort не меняется). Записывает features.multi_agent_v2.multi_agent_mode_hint_text в config.toml.", "sub.ultraModeV2Required": "Требуется мультиагентная поверхность v2 — сначала включите multi_agent_v2 и выберите v2 в переключателе режима субагентов.", - "sub.ultraModeText": "Текст делегирования ультра-режима", + "sub.ultraModeText": "Текст проактивного делегирования", "sub.ultraModePreset": "Восстановить пресет", - "sub.ultraModeLoadFail": "Не удалось загрузить настройки ультра-режима — работает ли прокси?", - "sub.ultraModeSaveFail": "Не удалось сохранить настройки ультра-режима", - "sub.ultraModeSaved": "Ультра-режим сохранён. Применяется к новым сеансам Codex.", + "sub.ultraModeLoadFail": "Не удалось загрузить настройки проактивного делегирования — работает ли прокси?", + "sub.ultraModeSaveFail": "Не удалось сохранить настройки проактивного делегирования", + "sub.ultraModeSaved": "Проактивное делегирование сохранено. Применяется к новым сеансам Codex.", // logs "logs.title": "Журнал запросов", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index aa97b22ff2..f551a97d43 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -702,14 +702,14 @@ export const tr: Record = { "sub.workspace.selectModel": "Bir model seçin", "sub.workspace.selectModelDesc": "Detayları görmek için listeden bir model seçin.", "sub.workspace.selector": "Genel seçici", - "sub.ultraMode": "Ultra modu", + "sub.ultraMode": "Her zaman proaktif delegasyon", "sub.ultraModeHint": "Tüm modeller ve reasoning effort için Proactive çoklu ajan delegasyon politikasını etkinleştirir (reasoning effort değerini değiştirmez). config.toml dosyasına features.multi_agent_v2.multi_agent_mode_hint_text yazar.", "sub.ultraModeV2Required": "v2 çoklu ajan yüzeyi gerekir — önce multi_agent_v2'yi etkinleştirin ve alt ajan modu denetiminde v2'yi seçin.", - "sub.ultraModeText": "Ultra modu delegasyon metni", + "sub.ultraModeText": "Proaktif delegasyon metni", "sub.ultraModePreset": "Ön ayarı geri yükle", - "sub.ultraModeLoadFail": "Ultra modu ayarları yüklenemedi — proxy çalışıyor mu?", - "sub.ultraModeSaveFail": "Ultra modu ayarları kaydedilemedi", - "sub.ultraModeSaved": "Ultra modu kaydedildi. Yeni Codex oturumlarına uygulanır.", + "sub.ultraModeLoadFail": "Proaktif delegasyon ayarları yüklenemedi — proxy çalışıyor mu?", + "sub.ultraModeSaveFail": "Proaktif delegasyon ayarları kaydedilemedi", + "sub.ultraModeSaved": "Proaktif delegasyon kaydedildi. Yeni Codex oturumlarına uygulanır.", "sub.fallbackLabel": "Alt ajan yedek zinciri", "sub.fallbackHint": "Alt ajan modeli kullanılamadığında veya başarısız olduğunda sırayla denenecek modeller.", "sub.fallbackAdd": "Yedek model ekle…", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index db9829821d..ae0ee0517b 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -547,14 +547,14 @@ export const zhTW: Record = { "sub.moveUp": "上移 {m}", "sub.moveDown": "下移 {m}", "sub.removeAria": "移除 {m}", - "sub.ultraMode": "超級模式", + "sub.ultraMode": "始終主動委派", "sub.ultraModeHint": "為所有模型和推理力度啟用主動多代理委派策略(不改變推理力度本身)。將 features.multi_agent_v2.multi_agent_mode_hint_text 寫入 config.toml。", "sub.ultraModeV2Required": "需要 v2 多代理表面 — 請先啟用 multi_agent_v2,並在子代理模式控制項中選擇 v2。", - "sub.ultraModeText": "超級模式委派文字", + "sub.ultraModeText": "主動委派文字", "sub.ultraModePreset": "還原預設", - "sub.ultraModeLoadFail": "無法載入超級模式設定 — 代理是否在執行?", - "sub.ultraModeSaveFail": "儲存超級模式設定失敗", - "sub.ultraModeSaved": "超級模式已儲存。適用於新的 Codex 會話。", + "sub.ultraModeLoadFail": "無法載入主動委派設定 — 代理是否在執行?", + "sub.ultraModeSaveFail": "儲存主動委派設定失敗", + "sub.ultraModeSaved": "主動委派設定已儲存。適用於新的 Codex 會話。", "logs.title": "請求日誌", "logs.tabLogs": "日誌", "logs.tabDebug": "除錯", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index a13ff07973..ca92aaeafc 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -690,14 +690,14 @@ export const zh: Record = { "sub.workspace.selectModel": "选择模型", "sub.workspace.selectModelDesc": "从列表中选择一个模型以查看详情,并将其设为 spawn_agent 的精选模型。", "sub.workspace.selector": "公开选择器", - "sub.ultraMode": "超级模式", + "sub.ultraMode": "始终主动委派", "sub.ultraModeHint": "为所有模型和推理力度启用主动多代理委派策略(不改变推理力度本身)。将 features.multi_agent_v2.multi_agent_mode_hint_text 写入 config.toml。", "sub.ultraModeV2Required": "需要 v2 多代理表面 — 请先启用 multi_agent_v2,并在子代理模式控件中选择 v2。", - "sub.ultraModeText": "超级模式委派文本", + "sub.ultraModeText": "主动委派文本", "sub.ultraModePreset": "恢复预设", - "sub.ultraModeLoadFail": "无法加载超级模式设置 — 代理是否在运行?", - "sub.ultraModeSaveFail": "保存超级模式设置失败", - "sub.ultraModeSaved": "超级模式已保存。适用于新的 Codex 会话。", + "sub.ultraModeLoadFail": "无法加载主动委派设置 — 代理是否在运行?", + "sub.ultraModeSaveFail": "保存主动委派设置失败", + "sub.ultraModeSaved": "主动委派设置已保存。适用于新的 Codex 会话。", // logs "logs.title": "请求日志", diff --git a/gui/src/pages/Subagents.tsx b/gui/src/pages/Subagents.tsx index 0b9806066d..94a8df7086 100644 --- a/gui/src/pages/Subagents.tsx +++ b/gui/src/pages/Subagents.tsx @@ -10,6 +10,11 @@ import { useSubagentDelegation, type UltraModePatch, type UltraModeState } from type CachedSubagents = { available: string[]; chosen: string[]; fallback?: string[]; pollMs?: number; fallbackAvailable?: string[] }; +const UNLOADED_ULTRA_MODE: UltraModeState = { + enabled: false, hintText: null, recommendation: null, + multiAgentV2Enabled: false, multiAgentMode: "default", +}; + function seedSubagents(cacheKey: string): CachedSubagents | null { return readSessionListCache(cacheKey); } @@ -40,7 +45,9 @@ export default function Subagents({ apiBase }: { apiBase: string }) { /** Sync guard: state-only `busy` can miss clicks before the disabled re-render commits. */ const saveInFlight = useRef(false); const delegation = useSubagentDelegation(apiBase); - const [ultraMode, setUltraMode] = useState({ enabled: false, hintText: null, multiAgentV2Enabled: false, multiAgentMode: "default" }); + const [ultraState, setUltraState] = useState<{ apiBase: string; mode: UltraModeState } | null>(null); + const ultraModeCurrent = ultraState?.apiBase === apiBase; + const ultraMode = ultraModeCurrent ? ultraState.mode : UNLOADED_ULTRA_MODE; const [ultraSaving, setUltraSaving] = useState(false); const [ultraLoadFailed, setUltraLoadFailed] = useState(false); const ultraLoadGeneration = useRef(0); @@ -62,21 +69,31 @@ export default function Subagents({ apiBase }: { apiBase: string }) { multiAgentMode?: "v1" | "default" | "v2"; multiAgentModeHintText?: string | null; keepNativeChatGptOnV1?: boolean; + multiAgentModeHintRecommendation?: { text?: unknown; revision?: unknown }; }>(res, t("sub.ultraModeLoadFail")); if (!data) return false; if (signal?.aborted || generation !== ultraLoadGeneration.current || currentUltraApiBase.current !== apiBase) return false; setUltraLoadFailed(false); - setUltraMode({ + const rawRecommendation = data.multiAgentModeHintRecommendation; + const recommendation = rawRecommendation + && typeof rawRecommendation.text === "string" + && rawRecommendation.text.trim().length > 0 + && typeof rawRecommendation.revision === "string" + && rawRecommendation.revision.trim().length > 0 + ? { text: rawRecommendation.text, revision: rawRecommendation.revision } + : null; + setUltraState({ apiBase, mode: { enabled: data.enabled ?? false, loaded: true, keepNativeChatGptOnV1: data.keepNativeChatGptOnV1 === true, hintText: data.multiAgentModeHintText ?? null, + recommendation, // Ultra mode replaces Codex's effort-derived policy for every model. The // `default` surface still preserves upstream V1 pins (for example luna), // so only an explicitly forced V2 catalog is an effective surface here. multiAgentV2Enabled: data.enabled === true && data.multiAgentMode === "v2", multiAgentMode: data.multiAgentMode ?? "default", - }); + } }); return true; }, [apiBase, t]); @@ -95,7 +112,7 @@ export default function Subagents({ apiBase }: { apiBase: string }) { }, [loadUltraMode, t]); const saveUltraMode = async (patch: UltraModePatch) => { - if (ultraSaving) return; + if (ultraSaving || !ultraModeCurrent || currentUltraApiBase.current !== apiBase) return; const requestApiBase = apiBase; setUltraSaving(true); setStatus(""); @@ -343,7 +360,7 @@ export default function Subagents({ apiBase }: { apiBase: string }) { saving: delegation.saving, onSave: patch => { void delegation.save(patch); }, ultraMode, - ultraSaving, + ultraSaving: ultraSaving || !ultraModeCurrent, onUltraModeSave: patch => { void saveUltraMode(patch); }, ultraLoadFailed, onUltraModeRetry: () => { void retryUltraMode(); }, diff --git a/gui/src/pages/use-subagent-delegation.ts b/gui/src/pages/use-subagent-delegation.ts index 9baa5baa9a..1a7829dc51 100644 --- a/gui/src/pages/use-subagent-delegation.ts +++ b/gui/src/pages/use-subagent-delegation.ts @@ -20,11 +20,17 @@ export type DelegationPatch = { }; /** Ultra mode (Proactive delegation for every model/effort) via /api/v2. */ +export type UltraModeHintRecommendation = { + text: string; + revision: string; +}; + export type UltraModeState = { loaded?: boolean; keepNativeChatGptOnV1?: boolean; enabled: boolean; hintText: string | null; + recommendation: UltraModeHintRecommendation | null; multiAgentV2Enabled: boolean; /** The raw multi-agent mode; Subagents renders the v1/base/v2 switch from it. */ multiAgentMode: "v1" | "default" | "v2"; diff --git a/gui/tests/multi-agent-guidance.test.tsx b/gui/tests/multi-agent-guidance.test.tsx index 8907406f0e..a2f2af5259 100644 --- a/gui/tests/multi-agent-guidance.test.tsx +++ b/gui/tests/multi-agent-guidance.test.tsx @@ -65,7 +65,7 @@ function props(overrides: Partial = {}): Props { guidanceEnabled: false, syncCodexDefaults: true, onSave: (patch) => { requests.push(patch); }, - ultraMode: { enabled: false, hintText: null, multiAgentV2Enabled: false, multiAgentMode: "default" }, + ultraMode: { enabled: false, hintText: null, recommendation: null, multiAgentV2Enabled: false, multiAgentMode: "default" }, fallback: [], fallbackPollMs: 60000, fallbackBusy: false, @@ -150,3 +150,32 @@ test("sends model clearing through the shared save path", async () => { expect(requests).toEqual([{ model: null, effort: "high" }]); }); + +test("a recommendation-only refresh preserves the draft until Restore is chosen", async () => { + const current = props({ + ultraMode: { + enabled: true, hintText: "stored custom", multiAgentV2Enabled: true, multiAgentMode: "v2", + recommendation: { text: "old recommendation", revision: "old" }, + }, + onUltraModeSave: patch => { requests.push(patch); }, + }); + await mount(current); + const textarea = host.querySelector(".swi-ultra-mode-editor textarea")!; + await act(async () => { + Object.getOwnPropertyDescriptor(testWindow.HTMLTextAreaElement.prototype, "value")!.set!.call(textarea, "unsaved custom draft"); + textarea.dispatchEvent(new testWindow.Event("input", { bubbles: true })); + textarea.dispatchEvent(new testWindow.Event("change", { bubbles: true })); + }); + await act(async () => { + root!.render(); + }); + expect(textarea.value).toBe("unsaved custom draft"); + expect(requests).toHaveLength(0); + const restore = [...host.querySelectorAll(".swi-ultra-mode-editor button")] + .find(button => button.textContent?.trim() === "Restore preset")!; + await act(async () => { restore.click(); }); + expect(textarea.value).toBe("new recommendation"); + expect(requests).toHaveLength(0); +}); diff --git a/gui/tests/subagents-ultra-mode.test.tsx b/gui/tests/subagents-ultra-mode.test.tsx index 6c2c1d8719..22cfc459be 100644 --- a/gui/tests/subagents-ultra-mode.test.tsx +++ b/gui/tests/subagents-ultra-mode.test.tsx @@ -1,9 +1,8 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; import { Window } from "happy-dom"; import { act } from "react"; -import { createRoot, type Root } from "react-dom/client"; +import type { Root } from "react-dom/client"; import Subagents from "../src/pages/Subagents"; -import { ULTRA_MODE_PRESET } from "../src/components/subagents-workspace/SubagentDelegationSection"; import { LanguageProvider } from "../src/i18n/provider"; const globals = ["document", "window", "navigator", "localStorage", "fetch", "IS_REACT_ACT_ENVIRONMENT"] as const; @@ -14,6 +13,7 @@ let root: Root | null = null; let v2Responses: Array<{ ok: boolean; body: unknown; status?: number }> = []; let v2Call = 0; let requests: Array<{ url: string; init?: RequestInit }> = []; +const recommendation = { text: "server-supplied proactive policy", revision: "test-policy-v1" }; function response(body: unknown, ok = true, status = 200): Response { return { @@ -75,6 +75,7 @@ afterEach(async () => { }); async function mount(apiBase = "") { + const { createRoot } = await import("react-dom/client"); await act(async () => { root = createRoot(container); root.render( @@ -88,13 +89,13 @@ async function mount(apiBase = "") { function ultraSwitch(): HTMLButtonElement { const button = Array.from(container.querySelectorAll("button")) - .find(candidate => candidate.getAttribute("aria-label") === "Ultra mode"); - if (!button) throw new Error("Ultra mode switch not found"); + .find(candidate => candidate.getAttribute("aria-label") === "Always proactive delegation"); + if (!button) throw new Error("Always proactive delegation switch not found"); return button as HTMLButtonElement; } test("does not enable Ultra mode for the default surface even when V2 is enabled", async () => { - v2Responses = [{ ok: true, body: { enabled: true, multiAgentMode: "default", multiAgentModeHintText: null } }]; + v2Responses = [{ ok: true, body: { enabled: true, multiAgentMode: "default", multiAgentModeHintText: null, multiAgentModeHintRecommendation: recommendation } }]; await mount(); expect(ultraSwitch().disabled).toBe(true); @@ -104,13 +105,13 @@ test("does not enable Ultra mode for the default surface even when V2 is enabled test("clears the page load error after a successful Ultra mode retry", async () => { v2Responses = [ { ok: false, body: { error: "temporary failure" }, status: 503 }, - { ok: true, body: { enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null } }, + { ok: true, body: { enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null, multiAgentModeHintRecommendation: recommendation } }, ]; await mount(); - expect(container.textContent).toContain("Failed to load Ultra mode settings"); + expect(container.textContent).toContain("Failed to load proactive delegation settings"); const ultraErrorRow = Array.from(container.querySelectorAll(".swi-delegation-row")) - .find(row => row.textContent?.includes("Failed to load Ultra mode settings")); + .find(row => row.textContent?.includes("Failed to load proactive delegation settings")); const retry = ultraErrorRow?.querySelector("button"); expect(retry).toBeTruthy(); @@ -118,18 +119,156 @@ test("clears the page load error after a successful Ultra mode retry", async () await act(async () => { await new Promise(resolve => setTimeout(resolve, 20)); }); expect(v2Call).toBe(2); - expect(container.textContent).not.toContain("Failed to load Ultra mode settings"); + expect(container.textContent).not.toContain("Failed to load proactive delegation settings"); expect(ultraSwitch().disabled).toBe(false); }); -test("uses the complete canonical proactive delegation preset", () => { - expect(ULTRA_MODE_PRESET).toBe([ - "Proactive multi-agent delegation is active.", - "Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies.", - "Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently.", - "Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself.", - "This mode remains active until a later multi-agent mode developer message changes it.", - ].join(" ")); +test("enabling Ultra mode uses the server-supplied recommendation", async () => { + v2Responses = [{ ok: true, body: { enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null, multiAgentModeHintRecommendation: recommendation } }]; + await mount(); + + await act(async () => { ultraSwitch().click(); }); + + const request = requests.find(item => item.init?.method === "PUT" && new URL(item.url, "http://localhost/").pathname === "/api/v2"); + expect(JSON.parse(String(request?.init?.body))).toEqual({ multiAgentModeHintText: recommendation.text }); +}); + +test("an older server without a recommendation disables only preset installation", async () => { + v2Responses = [{ ok: true, body: { enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null } }]; + await mount(); + + expect(ultraSwitch().disabled).toBe(true); + expect(ultraSwitch().getAttribute("aria-pressed")).toBe("false"); +}); + +test.each([ + { text: "", revision: "r1" }, + { text: "valid", revision: " " }, + { text: 42, revision: "r1" }, +])("malformed server recommendations cannot install a preset: %j", async malformed => { + v2Responses = [{ ok: true, body: { + enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null, + multiAgentModeHintRecommendation: malformed, + } }]; + await mount(); + + expect(ultraSwitch().disabled).toBe(true); + await act(async () => { ultraSwitch().click(); }); + expect(requests.filter(item => item.init?.method === "PUT")).toHaveLength(0); +}); + +test("an older server preserves an existing custom hint and still allows clearing it", async () => { + v2Responses = [{ ok: true, body: { enabled: true, multiAgentMode: "v2", multiAgentModeHintText: "custom policy" } }]; + await mount(); + + expect(ultraSwitch().disabled).toBe(false); + expect(ultraSwitch().getAttribute("aria-pressed")).toBe("true"); + await act(async () => { ultraSwitch().click(); }); + + const request = requests.find(item => item.init?.method === "PUT" && new URL(item.url, "http://localhost/").pathname === "/api/v2"); + expect(JSON.parse(String(request?.init?.body))).toEqual({ multiAgentModeHintText: null }); +}); + +test.each([undefined, { text: "", revision: "r1" }])("custom hints remain editable without a valid recommendation: %j", async unavailable => { + v2Responses = [{ ok: true, body: { + enabled: true, multiAgentMode: "v2", multiAgentModeHintText: "custom policy", + multiAgentModeHintRecommendation: unavailable, + } }]; + await mount(); + const editor = container.querySelector(".swi-ultra-mode-editor")!; + const textarea = editor.querySelector("textarea")!; + const restore = [...editor.querySelectorAll("button")].find(button => button.textContent?.trim() === "Restore preset")!; + const save = [...editor.querySelectorAll("button")].find(button => button.textContent?.trim() === "Save")!; + const custom = " my custom policy\nwith a preserved trailing space "; + expect(restore.disabled).toBe(true); + await act(async () => { + Object.getOwnPropertyDescriptor(testWindow.HTMLTextAreaElement.prototype, "value")!.set!.call(textarea, custom); + textarea.dispatchEvent(new testWindow.Event("input", { bubbles: true })); + textarea.dispatchEvent(new testWindow.Event("change", { bubbles: true })); + }); + expect(requests.filter(item => item.init?.method === "PUT")).toHaveLength(0); + await act(async () => { save.click(); }); + const puts = requests.filter(item => item.init?.method === "PUT"); + expect(puts).toHaveLength(1); + expect(JSON.parse(String(puts[0].init?.body))).toEqual({ multiAgentModeHintText: custom }); +}); + +test("a custom hint loads without writing and restore stays local until Save", async () => { + v2Responses = [{ ok: true, body: { + enabled: true, + multiAgentMode: "v2", + multiAgentModeHintText: "custom policy", + multiAgentModeHintRecommendation: recommendation, + } }]; + await mount(); + + const editor = container.querySelector(".swi-ultra-mode-editor"); + const textarea = editor?.querySelector("textarea") as HTMLTextAreaElement | null; + const restore = Array.from(editor?.querySelectorAll("button") ?? []) + .find(button => button.textContent?.trim() === "Restore preset"); + const save = Array.from(editor?.querySelectorAll("button") ?? []) + .find(button => button.textContent?.trim() === "Save"); + + expect(textarea?.value).toBe("custom policy"); + expect(requests.filter(item => item.init?.method === "PUT")).toHaveLength(0); + + await act(async () => { (restore as HTMLButtonElement).click(); }); + expect(textarea?.value).toBe(recommendation.text); + expect(requests.filter(item => item.init?.method === "PUT")).toHaveLength(0); + + await act(async () => { (save as HTMLButtonElement).click(); }); + const put = requests.find(item => item.init?.method === "PUT" && new URL(item.url, "http://localhost/").pathname === "/api/v2"); + expect(JSON.parse(String(put?.init?.body))).toEqual({ multiAgentModeHintText: recommendation.text }); +}); + +test.each([ + ["missing", undefined], + ["malformed", { text: "", revision: "b1" }], + ["valid", { text: "server-B policy", revision: "b1" }], +] as const)("server switches cannot install or restore another server's preset (%s)", async (_kind, nextRecommendation) => { + let releaseNext!: (value: Response) => void; + const nextRead = new Promise(resolve => { releaseNext = resolve; }); + const nextState = { + enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null, + multiAgentModeHintRecommendation: nextRecommendation, + }; + Object.defineProperty(globalThis, "fetch", { + configurable: true, + value: async (url: string, init?: RequestInit) => { + requests.push({ url: String(url), init }); + const path = new URL(String(url), "http://localhost/").pathname; + if (path === "/old/api/v2") return response({ + enabled: true, multiAgentMode: "v2", multiAgentModeHintText: "custom-A policy", + multiAgentModeHintRecommendation: recommendation, + }); + if (path === "/new/api/v2") return init?.method === "PUT" ? response(nextState) : nextRead; + if (path.endsWith("/api/subagent-models")) return response({ available: [], chosen: [] }); + if (path.endsWith("/api/subagent-model-fallback")) return response({ available: [], models: [], pollMs: 60_000 }); + if (path.endsWith("/api/injection-model")) return response({ available: [], efforts: [] }); + return response({}); + }, + }); + await mount("/old"); + expect(container.querySelector(".swi-ultra-mode-editor textarea")?.value).toBe("custom-A policy"); + await act(async () => { root!.render(); }); + + expect(ultraSwitch().disabled).toBe(true); + expect(container.querySelector(".swi-ultra-mode-editor")).toBeNull(); + await act(async () => { ultraSwitch().click(); }); + expect(requests.filter(item => item.init?.method === "PUT")).toHaveLength(0); + + await act(async () => { releaseNext(response(nextState)); await nextRead; }); + const valid = Boolean(nextRecommendation?.text); + expect(ultraSwitch().disabled).toBe(!valid); + await act(async () => { ultraSwitch().click(); }); + const puts = requests.filter(item => item.init?.method === "PUT"); + if (valid) { + expect(puts).toHaveLength(1); + expect(puts[0]?.url).toBe("/new/api/v2"); + expect(JSON.parse(String(puts[0]?.init?.body))).toEqual({ multiAgentModeHintText: nextRecommendation!.text }); + } else { + expect(puts).toHaveLength(0); + } }); test("a save refresh from an old API server cannot overwrite a newer server", async () => { @@ -143,7 +282,7 @@ test("a save refresh from an old API server cannot overwrite a newer server", as if (path === "/old/api/v2") { if (init?.method === "PUT") return response({ ok: true }); oldGets++; - if (oldGets === 1) return response({ enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null }); + if (oldGets === 1) return response({ enabled: true, multiAgentMode: "v2", multiAgentModeHintText: null, multiAgentModeHintRecommendation: recommendation }); return oldRefresh; } if (path === "/new/api/v2") return response({ enabled: false, multiAgentMode: "default", multiAgentModeHintText: null }); @@ -170,7 +309,7 @@ test("a save refresh from an old API server cannot overwrite a newer server", as expect(ultraSwitch().disabled).toBe(true); await act(async () => { - releaseOldRefresh(response({ enabled: true, multiAgentMode: "v2", multiAgentModeHintText: ULTRA_MODE_PRESET })); + releaseOldRefresh(response({ enabled: true, multiAgentMode: "v2", multiAgentModeHintText: recommendation.text, multiAgentModeHintRecommendation: recommendation })); await oldRefresh; await new Promise(resolve => setTimeout(resolve, 10)); }); diff --git a/src/codex/features.ts b/src/codex/features.ts index 9875a6ea2c..a52e9fc46e 100644 --- a/src/codex/features.ts +++ b/src/codex/features.ts @@ -38,6 +38,7 @@ import { AtomicWriteResidualTempError, AtomicWriteSecretResidualError, atomicWri import { forgetEphemeralSecretPath } from "../lib/windows-secret-acl"; import { CODEX_CONFIG_PATH } from "./paths"; import { resolveAndPersistCodexRuntime } from "./runtime"; +import { canonicalizeOpenCodexModeHint } from "./multi-agent-mode-policy"; /** Upstream codex-rs feature key: allow `request_user_input` in Default mode. */ export const DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY = "default_mode_request_user_input"; @@ -1091,7 +1092,8 @@ export function setMultiAgentModeHintText(value: string | null, configPath?: str }; } } - return setV2StringField("multi_agent_mode_hint_text", value, configPath); + const canonicalValue = value === null ? null : canonicalizeOpenCodexModeHint(value); + return setV2StringField("multi_agent_mode_hint_text", canonicalValue, configPath); } export const MODE_HINT_CAPABILITY_CACHE_MAX_ENTRIES = 8; diff --git a/src/codex/multi-agent-mode-policy.ts b/src/codex/multi-agent-mode-policy.ts new file mode 100644 index 0000000000..395dc72507 --- /dev/null +++ b/src/codex/multi-agent-mode-policy.ts @@ -0,0 +1,24 @@ +export const MULTI_AGENT_MODE_HINT_RECOMMENDATION = { + revision: "proactive-trigger-v1", + text: [ + "Proactive multi-agent delegation is active.", + "Only the delegation trigger changes: a separate explicit request is no longer required.", + "All existing user, authority, task-scope, and collaboration-tool rules continue to apply.", + "Delegate eligible independent work when parallel execution could materially improve speed or quality.", + "User requests override this hint.", + "This mode remains active until a later multi-agent mode developer message changes it.", + ].join(" "), +} as const; + +/** Byte-exact presets previously written by OpenCodex dashboard releases. */ +export const LEGACY_OPENCODEX_MODE_HINTS = [ + "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.", + "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently. Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself. This mode remains active until a later multi-agent mode developer message changes it.", +] as const; + +/** Upgrade only known OpenCodex-owned values; user-authored variants stay byte-identical. */ +export function canonicalizeOpenCodexModeHint(text: string): string { + return LEGACY_OPENCODEX_MODE_HINTS.some(legacy => legacy === text) + ? MULTI_AGENT_MODE_HINT_RECOMMENDATION.text + : text; +} diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index 561d00a080..d7617c5884 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -40,6 +40,7 @@ import { clearThreadAccountMap } from "../../codex/routing"; import { primeCodexPoolQuotas } from "../../codex/auth-api"; import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap"; import { resolveCodexHomeDir } from "../../codex/home"; +import { MULTI_AGENT_MODE_HINT_RECOMMENDATION } from "../../codex/multi-agent-mode-policy"; import { readUsageEntries } from "../../usage/log"; import { getUsageDebugLogEntries } from "../../usage/debug"; import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary"; @@ -248,6 +249,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise agentsMaxDepth: getAgentsMaxDepth(), subagentDeveloperInstructions: getSubagentDeveloperInstructions(), multiAgentModeHintText: getMultiAgentModeHintText(), + multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION, // max_depth is V1-only upstream; this is the global-flag statement, derived // server-side so no client can present it as an effective V2 limit. agentsMaxDepthAppliesWhenV2Disabled: !enabled, @@ -421,6 +423,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise agentsMaxDepth: getAgentsMaxDepth(), subagentDeveloperInstructions: getSubagentDeveloperInstructions(), multiAgentModeHintText: getMultiAgentModeHintText(), + multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION, agentsMaxDepthAppliesWhenV2Disabled: !enabled, warnings, catalogRefresh, diff --git a/tests/codex-integration/codex-v2-gate.test.ts b/tests/codex-integration/codex-v2-gate.test.ts index 6e8b6a18c1..39ec9a342c 100644 --- a/tests/codex-integration/codex-v2-gate.test.ts +++ b/tests/codex-integration/codex-v2-gate.test.ts @@ -44,10 +44,18 @@ import { v2TotalLimitToV1ChildLimit, } from "../../src/codex/features"; import { resetCodexRuntimeResolveCacheForTests, setCodexRuntimeResolveCacheForTests } from "../../src/codex/runtime"; +import { MULTI_AGENT_MODE_HINT_RECOMMENDATION } from "../../src/codex/multi-agent-mode-policy"; import { cmdV2, codexFeaturesInvocation, v2StatusLine, multiAgentModeLine } from "../../src/cli/v2"; import { handleManagementAPI } from "../../src/server/management-api"; import { catalogConvergenceFactory } from "../helpers/catalog-convergence"; +// Independently pinned release presets: removing a production compatibility +// entry must not silently remove its regression case too. +const RELEASED_MODE_HINTS = [ + "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.", + "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently. Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself. This mode remains active until a later multi-agent mode developer message changes it.", +] as const; + function template(): Record { return { slug: "gpt-5.5", @@ -423,6 +431,19 @@ describe("multi_agent_mode_hint_text reader/writer", () => { expect(readFileSync(path, "utf8")).toBe(before); }); + test("writer upgrades exact released presets while preserving user-edited text", () => { + for (const legacy of RELEASED_MODE_HINTS) { + const path = fixtureConfig(TABLE); + expect(setMultiAgentModeHintText(legacy, path)).toEqual({ ok: true, changed: true }); + expect(getMultiAgentModeHintText(path)).toBe(MULTI_AGENT_MODE_HINT_RECOMMENDATION.text); + } + for (const custom of [`${RELEASED_MODE_HINTS[0]} `, `${RELEASED_MODE_HINTS[1]} Ask before delegating.`]) { + const path = fixtureConfig(TABLE); + expect(setMultiAgentModeHintText(custom, path)).toEqual({ ok: true, changed: true }); + expect(getMultiAgentModeHintText(path)).toBe(custom); + } + }); + test("writer clears with null: removes the key, keeps siblings", () => { const path = fixtureConfig("[features.multi_agent_v2]\nenabled = true\nmulti_agent_mode_hint_text = \"Proactive delegation\"\nmax_concurrent_threads_per_session = 17\n"); expect(setMultiAgentModeHintText(null, path)).toEqual({ ok: true, changed: true }); @@ -1348,6 +1369,7 @@ describe("management API parity surface for the WP2 keys", () => { agentsMaxDepth: 2, subagentDeveloperInstructions: null, multiAgentModeHintText: null, + multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION, agentsMaxDepthAppliesWhenV2Disabled: true, }); const v2Path = fixtureConfig("[features.multi_agent_v2]\nenabled = true\n"); @@ -1357,6 +1379,37 @@ describe("management API parity surface for the WP2 keys", () => { }); }); + test.each(RELEASED_MODE_HINTS)("GET preserves a released preset until an explicit hint save: %s", async legacy => { + const initial = `[features.multi_agent_v2]\nenabled = false\n# keep adjacent setting\nmax_concurrent_threads_per_session = 17\nmulti_agent_mode_hint_text = ${JSON.stringify(legacy)}\n`; + await withConfig(initial, async (path, deps) => { + const before = readFileSync(path, "utf8"); + const get = await handleManagementAPI(new Request("http://localhost/api/v2"), new URL("http://localhost/api/v2"), config, deps); + expect(await get?.json()).toMatchObject({ + multiAgentModeHintText: legacy, + multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION, + }); + expect(readFileSync(path, "utf8")).toBe(before); + + const unrelated = await handleManagementAPI(put({ agentsEnabled: false }), new URL("http://localhost/api/v2"), config, deps); + expect(unrelated?.status).toBe(200); + expect(getMultiAgentModeHintText(path)).toBe(legacy); + + const saved = await handleManagementAPI(put({ multiAgentModeHintText: legacy }), new URL("http://localhost/api/v2"), config, deps); + expect(saved?.status).toBe(200); + expect(await saved?.json()).toMatchObject({ + multiAgentModeHintText: MULTI_AGENT_MODE_HINT_RECOMMENDATION.text, + multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION, + }); + expect(getMultiAgentModeHintText(path)).toBe(MULTI_AGENT_MODE_HINT_RECOMMENDATION.text); + expect(readFileSync(path, "utf8")).toContain("# keep adjacent setting"); + expect(getMaxConcurrentThreads(path)).toBe(17); + + const after = readFileSync(path, "utf8"); + expect(setMultiAgentModeHintText(legacy, path)).toEqual({ ok: true, changed: false }); + expect(readFileSync(path, "utf8")).toBe(after); + }); + }); + test("PUT writes each new field independently and re-reads them", async () => { await withConfig("[features.multi_agent_v2]\nenabled = false\n", async (path, deps) => { const onlyNew = await handleManagementAPI(put({ agentsEnabled: false }), new URL("http://localhost/api/v2"), config, deps);