Retire the personality feature flag and document deprecated settings (#45809)

## What changed

- Accept and ignore `features.personality` in user configuration, profiles, and managed requirements.
- Remove feature gating from `personality = "none"`, which strips the literal `# Personality` section when preparing model catalog instructions. Preserve explicit base instructions and existing thread instructions.
- Document deprecated personality fields in configuration, app-server schemas, and the Python SDK: `friendly` and `pragmatic` no longer select a style, and `supportsPersonality` is always `false`.

## Testing

Add regression coverage for ignored legacy flag values and managed requirements, personality opt-out behavior with the flag absent or set to either value, role overrides, and preservation of explicit base instructions, including empty strings.

GitOrigin-RevId: 4e12c66b42bfc59d6f151a5b3c28fadc6654ae99
This commit is contained in:
rhan-oai
2026-09-15 23:39:40 +00:00
committed by copyberry
parent 4d2807023a
commit 883af106b9
54 changed files with 389 additions and 264 deletions
+7 -4
View File
@@ -535,7 +535,7 @@
]
},
"reloadUserConfig": {
"description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults are not reloaded.",
"description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, and service-tier defaults are not reloaded. The deprecated personality setting is also not reloaded.",
"type": "boolean"
}
},
@@ -2335,6 +2335,7 @@
"type": "object"
},
"Personality": {
"description": "Deprecated: `friendly` and `pragmatic` no longer select a style.",
"enum": [
"none",
"friendly",
@@ -4941,7 +4942,8 @@
{
"type": "null"
}
]
],
"description": "@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
},
"sandbox": {
"anyOf": [
@@ -5267,7 +5269,8 @@
{
"type": "null"
}
]
],
"description": "@deprecated `friendly` and `pragmatic` no longer select a style."
},
"sandbox": {
"anyOf": [
@@ -5541,7 +5544,7 @@
"type": "null"
}
],
"description": "Override the personality for this turn and subsequent turns."
"description": "@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
},
"sandboxPolicy": {
"anyOf": [
@@ -3250,6 +3250,7 @@
]
},
"Personality": {
"description": "Deprecated: `friendly` and `pragmatic` no longer select a style.",
"enum": [
"none",
"friendly",
@@ -5983,7 +5984,8 @@
{
"type": "null"
}
]
],
"description": "@deprecated Reports the saved setting; `friendly` and `pragmatic` no longer select a style."
},
"sandboxPolicy": {
"$ref": "#/definitions/SandboxPolicy"
@@ -9463,7 +9463,7 @@
]
},
"reloadUserConfig": {
"description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults are not reloaded.",
"description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, and service-tier defaults are not reloaded. The deprecated personality setting is also not reloaded.",
"type": "boolean"
}
},
@@ -15256,6 +15256,7 @@
},
"supportsPersonality": {
"default": false,
"description": "@deprecated Always false; models no longer support personality selection.",
"type": "boolean"
},
"upgrade": {
@@ -15969,6 +15970,7 @@
"type": "object"
},
"Personality": {
"description": "Deprecated: `friendly` and `pragmatic` no longer select a style.",
"enum": [
"none",
"friendly",
@@ -23389,7 +23391,8 @@
{
"type": "null"
}
]
],
"description": "@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
},
"sandbox": {
"anyOf": [
@@ -23925,7 +23928,8 @@
{
"type": "null"
}
]
],
"description": "@deprecated Reports the saved setting; `friendly` and `pragmatic` no longer select a style."
},
"sandboxPolicy": {
"$ref": "#/definitions/v2/SandboxPolicy"
@@ -24108,7 +24112,8 @@
{
"type": "null"
}
]
],
"description": "@deprecated `friendly` and `pragmatic` no longer select a style."
},
"sandbox": {
"anyOf": [
@@ -25258,7 +25263,7 @@
"type": "null"
}
],
"description": "Override the personality for this turn and subsequent turns."
"description": "@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
},
"sandboxPolicy": {
"anyOf": [
@@ -5343,7 +5343,7 @@
]
},
"reloadUserConfig": {
"description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults are not reloaded.",
"description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, and service-tier defaults are not reloaded. The deprecated personality setting is also not reloaded.",
"type": "boolean"
}
},
@@ -11308,6 +11308,7 @@
},
"supportsPersonality": {
"default": false,
"description": "@deprecated Always false; models no longer support personality selection.",
"type": "boolean"
},
"upgrade": {
@@ -12021,6 +12022,7 @@
"type": "object"
},
"Personality": {
"description": "Deprecated: `friendly` and `pragmatic` no longer select a style.",
"enum": [
"none",
"friendly",
@@ -21103,7 +21105,8 @@
{
"type": "null"
}
]
],
"description": "@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
},
"sandbox": {
"anyOf": [
@@ -21639,7 +21642,8 @@
{
"type": "null"
}
]
],
"description": "@deprecated Reports the saved setting; `friendly` and `pragmatic` no longer select a style."
},
"sandboxPolicy": {
"$ref": "#/definitions/SandboxPolicy"
@@ -21822,7 +21826,8 @@
{
"type": "null"
}
]
],
"description": "@deprecated `friendly` and `pragmatic` no longer select a style."
},
"sandbox": {
"anyOf": [
@@ -22972,7 +22977,7 @@
"type": "null"
}
],
"description": "Override the personality for this turn and subsequent turns."
"description": "@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
},
"sandboxPolicy": {
"anyOf": [
@@ -47,7 +47,7 @@
]
},
"reloadUserConfig": {
"description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults are not reloaded.",
"description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, and service-tier defaults are not reloaded. The deprecated personality setting is also not reloaded.",
"type": "boolean"
}
},
@@ -140,6 +140,7 @@
},
"supportsPersonality": {
"default": false,
"description": "@deprecated Always false; models no longer support personality selection.",
"type": "boolean"
},
"upgrade": {
@@ -470,6 +470,7 @@
]
},
"Personality": {
"description": "Deprecated: `friendly` and `pragmatic` no longer select a style.",
"enum": [
"none",
"friendly",
@@ -1545,7 +1546,8 @@
{
"type": "null"
}
]
],
"description": "@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
},
"sandbox": {
"anyOf": [
@@ -140,6 +140,7 @@
"type": "string"
},
"Personality": {
"description": "Deprecated: `friendly` and `pragmatic` no longer select a style.",
"enum": [
"none",
"friendly",
@@ -355,7 +356,8 @@
{
"type": "null"
}
]
],
"description": "@deprecated Reports the saved setting; `friendly` and `pragmatic` no longer select a style."
},
"sandboxPolicy": {
"$ref": "#/definitions/SandboxPolicy"
@@ -220,6 +220,7 @@
]
},
"Personality": {
"description": "Deprecated: `friendly` and `pragmatic` no longer select a style.",
"enum": [
"none",
"friendly",
@@ -373,7 +374,8 @@
{
"type": "null"
}
]
],
"description": "@deprecated `friendly` and `pragmatic` no longer select a style."
},
"sandbox": {
"anyOf": [
@@ -312,6 +312,7 @@
"type": "string"
},
"Personality": {
"description": "Deprecated: `friendly` and `pragmatic` no longer select a style.",
"enum": [
"none",
"friendly",
@@ -827,7 +828,7 @@
"type": "null"
}
],
"description": "Override the personality for this turn and subsequent turns."
"description": "@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
},
"sandboxPolicy": {
"anyOf": [
@@ -2,4 +2,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Deprecated: `friendly` and `pragmatic` no longer select a style.
*/
export type Personality = "none" | "friendly" | "pragmatic";
@@ -10,7 +10,7 @@ export type ConfigBatchWriteParams = { edits: Array<ConfigEdit>,
filePath?: string | null, expectedVersion?: string | null,
/**
* When true, hot-reload updated runtime settings into loaded threads after writing.
* Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and
* personality defaults are not reloaded.
* Session-static model, reasoning-effort, Plan-mode reasoning-effort, and service-tier
* defaults are not reloaded. The deprecated personality setting is also not reloaded.
*/
reloadUserConfig?: boolean, };
+5 -1
View File
@@ -10,7 +10,11 @@ import type { ModelUpgradeInfo } from "./ModelUpgradeInfo";
import type { MultiAgentVersion } from "./MultiAgentVersion";
import type { ReasoningEffortOption } from "./ReasoningEffortOption";
export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, modelSpecialty: string | null, hidden: boolean, supportedReasoningEfforts: Array<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, supportsPersonality: boolean,
export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, modelSpecialty: string | null, hidden: boolean, supportedReasoningEfforts: Array<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>,
/**
* @deprecated Always false; models no longer support personality selection.
*/
supportsPersonality: boolean,
/**
* Multi-agent runtime declared by this model, when available.
*/
@@ -30,7 +30,11 @@ model?: string | null, modelProvider?: string | null, serviceTier?: string | nul
* Override where approval requests are routed for review on this thread
* and subsequent turns.
*/
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /**
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, /**
* @deprecated `friendly` and `pragmatic` no longer select a style.
* Changing this does not rewrite the thread's existing instructions.
*/
personality?: Personality | null, /**
* When true, return only thread metadata and live-resume state without
* populating `thread.turns`. This is useful when the client plans to call
* `thread/turns/list` immediately after resuming. Full-history hydration
@@ -14,4 +14,7 @@ import type { SandboxPolicy } from "./SandboxPolicy";
export type ThreadSettings = {/**
* Saved list of disabled plugin IDs. Does not yet filter plugin capabilities.
*/
disabledPluginIds: Array<string>, cwd: AbsolutePathBuf, approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, sandboxPolicy: SandboxPolicy, activePermissionProfile: ActivePermissionProfile | null, model: string, modelProvider: string, serviceTier: string | null, effort: ReasoningEffort | null, summary: ReasoningSummary | null, collaborationMode: CollaborationMode, personality: Personality | null};
disabledPluginIds: Array<string>, cwd: AbsolutePathBuf, approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, sandboxPolicy: SandboxPolicy, activePermissionProfile: ActivePermissionProfile | null, model: string, modelProvider: string, serviceTier: string | null, effort: ReasoningEffort | null, summary: ReasoningSummary | null, collaborationMode: CollaborationMode, /**
* @deprecated Reports the saved setting; `friendly` and `pragmatic` no longer select a style.
*/
personality: Personality | null};
@@ -13,7 +13,10 @@ export type ThreadStartParams = {model?: string | null, modelProvider?: string |
* Override where approval requests are routed for review on this thread
* and subsequent turns.
*/
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /**
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, /**
* @deprecated `friendly` and `pragmatic` no longer select a style.
*/
personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /**
* Optional client-supplied analytics source classification for this thread.
*/
threadSource?: ThreadSource | null};
@@ -50,7 +50,8 @@ effort?: ReasoningEffort | null, /**
* Override the reasoning summary for this turn and subsequent turns.
*/
summary?: ReasoningSummary | null, /**
* Override the personality for this turn and subsequent turns.
* @deprecated `friendly` and `pragmatic` no longer select a style.
* Changing this does not rewrite the thread's existing instructions.
*/
personality?: Personality | null, /**
* Optional JSON Schema used to constrain the final assistant message for
@@ -1089,8 +1089,8 @@ pub struct ConfigBatchWriteParams {
#[ts(optional = nullable)]
pub expected_version: Option<String>,
/// When true, hot-reload updated runtime settings into loaded threads after writing.
/// Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and
/// personality defaults are not reloaded.
/// Session-static model, reasoning-effort, Plan-mode reasoning-effort, and service-tier
/// defaults are not reloaded. The deprecated personality setting is also not reloaded.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub reload_user_config: bool,
}
@@ -131,6 +131,7 @@ pub struct Model {
pub default_reasoning_effort: ReasoningEffort,
#[serde(default = "default_input_modalities")]
pub input_modalities: Vec<InputModality>,
/// @deprecated Always false; models no longer support personality selection.
#[serde(default)]
pub supports_personality: bool,
/// Multi-agent runtime declared by this model, when available.
@@ -104,6 +104,7 @@ pub struct ThreadStartParams {
pub base_instructions: Option<String>,
#[ts(optional = nullable)]
pub developer_instructions: Option<String>,
/// @deprecated `friendly` and `pragmatic` no longer select a style.
#[ts(optional = nullable)]
pub personality: Option<Personality>,
/// @deprecated Ignored. Use Ultra reasoning effort for proactive multi-agent behavior.
@@ -286,7 +287,8 @@ pub struct ThreadSettingsUpdateParams {
#[experimental("thread/settings/update.multiAgentMode")]
#[ts(optional = nullable)]
pub multi_agent_mode: Option<MultiAgentMode>,
/// Override the personality for subsequent turns.
/// @deprecated `friendly` and `pragmatic` no longer select a style.
/// Changing this does not rewrite the thread's existing instructions.
#[ts(optional = nullable)]
pub personality: Option<Personality>,
}
@@ -318,6 +320,7 @@ pub struct ThreadSettings {
#[experimental("thread/settings.multiAgentMode")]
#[serde(default)]
pub multi_agent_mode: MultiAgentMode,
/// @deprecated Reports the saved setting; `friendly` and `pragmatic` no longer select a style.
pub personality: Option<Personality>,
}
@@ -409,6 +412,8 @@ pub struct ThreadResumeParams {
pub base_instructions: Option<String>,
#[ts(optional = nullable)]
pub developer_instructions: Option<String>,
/// @deprecated `friendly` and `pragmatic` no longer select a style.
/// Changing this does not rewrite the thread's existing instructions.
#[ts(optional = nullable)]
pub personality: Option<Personality>,
/// When true, return only thread metadata and live-resume state without
@@ -248,7 +248,8 @@ pub struct TurnStartParams {
/// Override the reasoning summary for this turn and subsequent turns.
#[ts(optional = nullable)]
pub summary: Option<ReasoningSummary>,
/// Override the personality for this turn and subsequent turns.
/// @deprecated `friendly` and `pragmatic` no longer select a style.
/// Changing this does not rewrite the thread's existing instructions.
#[ts(optional = nullable)]
pub personality: Option<Personality>,
/// Optional JSON Schema used to constrain the final assistant message for
+13
View File
@@ -231,6 +231,19 @@ Read the selection from `threadSettings.disabledPluginIds` in
across resume. Forks restore the selection from the history retained at the
requested fork boundary.
# Deprecated thread personality setting
`thread/start`, `thread/resume`, `thread/settings/update`, and `turn/start` still
accept `personality`, but `friendly` and `pragmatic` no longer select a style.
`model/list` returns `supportsPersonality: false` for every model.
`none` removes the literal `# Personality` section when Codex prepares
instructions from the model catalog, for example when starting a thread or
switching models. Setting `friendly` or `pragmatic` can replace a previous
`none` setting for that purpose. Changing the setting does not rewrite the
thread's existing instructions or change explicitly supplied base instructions.
The old `features.personality` flag is ignored.
# MCP server capabilities
`mcpServerStatus/list` returns `serverCapabilities` for each initialized MCP server
@@ -1535,7 +1535,7 @@ async fn write_value_rejects_feature_requirement_conflict() {
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"
[features]
personality = true
fast_mode = true
"#,
),
);
@@ -1543,7 +1543,7 @@ personality = true
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
key_path: "features.personality".to_string(),
key_path: "features.fast_mode".to_string(),
value: serde_json::json!(false),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
@@ -1558,7 +1558,7 @@ personality = true
assert!(
error
.to_string()
.contains("invalid value for `features`: `features.personality=false`"),
.contains("invalid value for `features`: `features.fast_mode=false`"),
"{error}"
);
assert_eq!(
@@ -11,7 +11,7 @@ fn mock_responses_config_composes_model_provider_features_and_extra_tables() {
.with_provider_base_url("http://127.0.0.1:1234/api/codex")
.with_approval_policy("on-request")
.with_sandbox_mode("workspace-write")
.enable_feature(Feature::Personality)
.enable_feature(Feature::FastMode)
.disable_feature(Feature::ShellSnapshot)
.with_root_config("chatgpt_base_url = \"http://127.0.0.1:1234\"")
.with_provider_config("requires_openai_auth = true")
@@ -28,7 +28,7 @@ fn mock_responses_config_composes_model_provider_features_and_extra_tables() {
"chatgpt_base_url = \"http://127.0.0.1:1234\"",
"model_provider = \"openai-custom\"",
"shell_snapshot = false",
"personality = true",
"fast_mode = true",
"[model_providers.openai-custom]\nname = \"OpenAI\"",
"base_url = \"http://127.0.0.1:1234/api/codex\"",
"requires_openai_auth = true",
@@ -44,10 +44,7 @@ fn legacy_mock_responses_writer_preserves_provider_auth_and_feature_overrides()
write_mock_responses_config_toml(
home.path(),
"http://127.0.0.1:1234",
&BTreeMap::from([
(Feature::Personality, true),
(Feature::ShellSnapshot, false),
]),
&BTreeMap::from([(Feature::FastMode, true), (Feature::ShellSnapshot, false)]),
/*auto_compact_limit*/ 321,
Some(true),
"openai",
@@ -62,7 +59,7 @@ fn legacy_mock_responses_writer_preserves_provider_auth_and_feature_overrides()
"model_auto_compact_token_limit = 321",
"openai_base_url = \"http://127.0.0.1:1234/v1\"",
"shell_snapshot = false",
"personality = true",
"fast_mode = true",
"[model_providers.openai]\nname = \"OpenAI\"",
"supports_websockets = false",
"requires_openai_auth = true",
@@ -487,6 +487,5 @@ fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io
MockResponsesConfig::new(server_uri)
.with_model(REQUESTED_MODEL)
.disable_feature(Feature::RemoteModels)
.enable_feature(Feature::Personality)
.write(codex_home)
}
@@ -1178,13 +1178,9 @@ stream_max_retries = 0
async fn thread_resume_preserves_goal_first_and_fork_approvals_reviewer() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri()).write(codex_home.path())?;
let config_path = codex_home.path().join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
std::fs::write(
&config_path,
config.replace("personality = true\n", "personality = true\ngoals = true\n"),
)?;
mock_responses_config(&server.uri())
.enable_feature(Feature::Goals)
.write(codex_home.path())?;
let (thread_id, fork_thread_id) = {
let mut mcp = TestAppServer::builder()
@@ -1271,6 +1267,7 @@ async fn thread_resume_preserves_goal_first_and_fork_approvals_reviewer() -> Res
(thread.id, fork_thread.id)
};
let config_path = codex_home.path().join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
std::fs::write(
config_path,
@@ -1943,13 +1940,9 @@ fn write_dev_permission_config(
async fn thread_goal_get_rejects_unmaterialized_thread() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri()).write(codex_home.path())?;
let config_path = codex_home.path().join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
std::fs::write(
&config_path,
config.replace("personality = true\n", "personality = true\ngoals = true\n"),
)?;
mock_responses_config(&server.uri())
.enable_feature(Feature::Goals)
.write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -2244,13 +2237,9 @@ async fn goal_first_live_thread_appears_in_state_db_thread_list() -> Result<()>
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
let codex_home_path = normalized_existing_path(codex_home.path())?;
mock_responses_config(&server.uri()).write(&codex_home_path)?;
let config_path = codex_home_path.join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
std::fs::write(
&config_path,
config.replace("personality = true\n", "personality = true\ngoals = true\n"),
)?;
mock_responses_config(&server.uri())
.enable_feature(Feature::Goals)
.write(&codex_home_path)?;
let sqlite_home = codex_home_path
.as_path()
@@ -3000,13 +2989,9 @@ async fn thread_resume_keeps_tool_paused_goal_paused() -> Result<()> {
])
.await;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri()).write(codex_home.path())?;
let config_path = codex_home.path().join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
std::fs::write(
&config_path,
config.replace("personality = true\n", "personality = true\ngoals = true\n"),
)?;
mock_responses_config(&server.uri())
.enable_feature(Feature::Goals)
.write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -3088,14 +3073,10 @@ async fn thread_resume_keeps_tool_paused_goal_paused() -> Result<()> {
async fn thread_goal_set_enforces_configured_maximum_token_budget() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri()).write(codex_home.path())?;
let config_path = codex_home.path().join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
let config = config.replace("personality = true\n", "personality = true\ngoals = true\n");
std::fs::write(
config_path,
format!("{config}\n[goals]\nmax_goal_token_budget = 200\n"),
)?;
mock_responses_config(&server.uri())
.enable_feature(Feature::Goals)
.with_extra_config("[goals]\nmax_goal_token_budget = 200")
.write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -3185,13 +3166,9 @@ async fn thread_goal_set_enforces_configured_maximum_token_budget() -> Result<()
async fn thread_goal_set_preserves_budget_limited_same_objective() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri()).write(codex_home.path())?;
let config_path = codex_home.path().join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
std::fs::write(
&config_path,
config.replace("personality = true\n", "personality = true\ngoals = true\n"),
)?;
mock_responses_config(&server.uri())
.enable_feature(Feature::Goals)
.write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -3275,13 +3252,9 @@ async fn thread_goal_set_preserves_budget_limited_same_objective() -> Result<()>
async fn thread_goal_set_persists_resumable_stopped_statuses() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri()).write(codex_home.path())?;
let config_path = codex_home.path().join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
std::fs::write(
&config_path,
config.replace("personality = true\n", "personality = true\ngoals = true\n"),
)?;
mock_responses_config(&server.uri())
.enable_feature(Feature::Goals)
.write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -3357,13 +3330,9 @@ async fn thread_goal_set_persists_resumable_stopped_statuses() -> Result<()> {
async fn thread_goal_set_edits_objective_without_resetting_usage() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri()).write(codex_home.path())?;
let config_path = codex_home.path().join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
std::fs::write(
&config_path,
config.replace("personality = true\n", "personality = true\ngoals = true\n"),
)?;
mock_responses_config(&server.uri())
.enable_feature(Feature::Goals)
.write(codex_home.path())?;
let thread_id = create_fake_rollout(
codex_home.path(),
"2025-01-05T12-00-00",
@@ -3686,14 +3655,9 @@ async fn thread_goal_lifecycle_emits_analytics_and_clear_deletes_goal() -> Resul
.await;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri())
.enable_feature(Feature::Goals)
.with_root_config(&format!(r#"chatgpt_base_url = "{}""#, server.uri()))
.write(codex_home.path())?;
let config_path = codex_home.path().join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
std::fs::write(
&config_path,
config.replace("personality = true\n", "personality = true\ngoals = true\n"),
)?;
mount_analytics_capture(&server, codex_home.path()).await?;
let mut mcp = TestAppServer::builder()
@@ -6263,9 +6227,7 @@ async fn thread_resume_accepts_deprecated_personality_override() -> Result<()> {
}
fn mock_responses_config(server_uri: &str) -> MockResponsesConfig {
MockResponsesConfig::new(server_uri)
.with_model("gpt-5.4")
.enable_feature(Feature::Personality)
MockResponsesConfig::new(server_uri).with_model("gpt-5.4")
}
#[allow(dead_code)]
@@ -842,9 +842,7 @@ async fn turn_start_sends_originator_header() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(responses).await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri())
.enable_feature(Feature::Personality)
.write(codex_home.path())?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -911,9 +909,7 @@ async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(responses).await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri())
.enable_feature(Feature::Personality)
.write(codex_home.path())?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -989,9 +985,7 @@ async fn turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills(
let server = create_mock_responses_server_sequence_unchecked(responses).await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri())
.enable_feature(Feature::Personality)
.write(codex_home.path())?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
write_models_cache(codex_home.path()).await?;
let cache_path = codex_home.path().join("models_cache.json");
let mut cache: serde_json::Value =
@@ -1826,9 +1820,7 @@ async fn turn_start_accepts_text_at_limit_with_mention_item() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(responses).await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri())
.enable_feature(Feature::Personality)
.write(codex_home.path())?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -1876,9 +1868,7 @@ async fn turn_start_accepts_text_at_limit_with_mention_item() -> Result<()> {
#[tokio::test]
async fn turn_start_rejects_combined_oversized_text_input() -> Result<()> {
let codex_home = TempDir::new()?;
MockResponsesConfig::new("http://localhost/unused")
.enable_feature(Feature::Personality)
.write(codex_home.path())?;
MockResponsesConfig::new("http://localhost/unused").write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -1969,9 +1959,7 @@ async fn turn_start_rejects_combined_oversized_text_input() -> Result<()> {
#[tokio::test]
async fn turn_start_rejects_invalid_permission_selection_before_starting_turn() -> Result<()> {
let codex_home = TempDir::new()?;
MockResponsesConfig::new("http://localhost/unused")
.enable_feature(Feature::Personality)
.write(codex_home.path())?;
MockResponsesConfig::new("http://localhost/unused").write(codex_home.path())?;
std::fs::write(
codex_home.path().join("managed_config.toml"),
"sandbox_mode = \"read-only\"\n",
@@ -2403,9 +2391,7 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<(
let server = create_mock_responses_server_sequence_unchecked(responses).await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri())
.enable_feature(Feature::Personality)
.write(codex_home.path())?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -2679,9 +2665,7 @@ async fn turn_start_accepts_deprecated_personality_override_v2() -> Result<()> {
let response_mock = responses::mount_sse_once(&server, body).await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri())
.enable_feature(Feature::Personality)
.write(codex_home.path())?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -2892,9 +2876,7 @@ async fn turn_start_ignores_personality_change_mid_thread_v2() -> Result<()> {
let response_mock = responses::mount_sse_sequence(&server, vec![sse1, sse2]).await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri())
.enable_feature(Feature::Personality)
.write(codex_home.path())?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -5762,9 +5744,7 @@ async fn turn_start_with_elevated_override_does_not_persist_project_trust() -> R
let server = create_mock_responses_server_sequence_unchecked(responses).await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri())
.enable_feature(Feature::Personality)
.write(codex_home.path())?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
let workspace = TempDir::new()?;
+1 -1
View File
@@ -383,7 +383,7 @@ pub struct ConfigToml {
/// Per-thread `config` overrides are accepted but do not reapply this (no-ops).
pub model_catalog_json: Option<AbsolutePathBuf>,
/// Optionally specify a personality for the model
/// Deprecated: `friendly` and `pragmatic` no longer select a style.
pub personality: Option<Personality>,
/// Optional explicit service tier request id for new turns (for example
+1
View File
@@ -39,6 +39,7 @@ pub struct ConfigProfile {
pub model_verbosity: Option<Verbosity>,
/// Optional path to a JSON model catalog (applied on startup only).
pub model_catalog_json: Option<AbsolutePathBuf>,
/// Deprecated: `friendly` and `pragmatic` no longer select a style.
pub personality: Option<Personality>,
pub chatgpt_base_url: Option<String>,
/// Optional path to a file containing model instructions.
+8 -2
View File
@@ -1164,7 +1164,12 @@
"type": "string"
},
"personality": {
"$ref": "#/definitions/Personality"
"allOf": [
{
"$ref": "#/definitions/Personality"
}
],
"description": "Deprecated: `friendly` and `pragmatic` no longer select a style."
},
"plan_mode_reasoning_effort": {
"$ref": "#/definitions/ReasoningEffort"
@@ -3200,6 +3205,7 @@
"type": "object"
},
"Personality": {
"description": "Deprecated: `friendly` and `pragmatic` no longer select a style.",
"enum": [
"none",
"friendly",
@@ -7028,7 +7034,7 @@
"$ref": "#/definitions/Personality"
}
],
"description": "Optionally specify a personality for the model"
"description": "Deprecated: `friendly` and `pragmatic` no longer select a style."
},
"plan_mode_reasoning_effort": {
"$ref": "#/definitions/ReasoningEffort"
+2 -5
View File
@@ -94,7 +94,6 @@ async fn apply_role_to_config_inner(
&& let Some(
feature @ (Feature::ShellTool
| Feature::Apps
| Feature::Personality
| Feature::Plugins
| Feature::MemoryTool
| Feature::RequestPermissionsTool),
@@ -222,10 +221,8 @@ mod role_overrides {
{
next_config.include_skill_instructions = false;
}
let strips_baked_personality = |config: &Config| {
config.features.enabled(Feature::Personality)
&& config.personality == Some(Personality::None)
};
let strips_baked_personality =
|config: &Config| config.personality == Some(Personality::None);
if strips_baked_personality(config) != strips_baked_personality(&next_config)
&& matches!(
config.base_instructions_provenance,
+16
View File
@@ -250,6 +250,22 @@ async fn apply_role_refreshes_model_instructions_only_when_personality_opt_out_c
},
false,
),
(
"none",
"[features]\npersonality = false",
BaseInstructionsProvenance::Model {
model: "parent-model".to_string(),
},
false,
),
(
"friendly",
"personality = \"none\"\n[features]\npersonality = false",
BaseInstructionsProvenance::Model {
model: "parent-model".to_string(),
},
true,
),
(
"friendly",
"personality = \"pragmatic\"",
+57 -8
View File
@@ -11201,7 +11201,7 @@ async fn feature_requirements_normalize_effective_feature_values() -> std::io::R
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"
[features]
personality = true
view_image = true
shell_tool = false
use_xaa = true
"#,
@@ -11210,7 +11210,7 @@ use_xaa = true
.build()
.await?;
assert!(config.features.enabled(Feature::Personality));
assert!(config.features.enabled(Feature::ViewImage));
assert!(!config.features.enabled(Feature::ShellTool));
assert!(
!config
@@ -11396,7 +11396,7 @@ async fn explicit_feature_config_is_normalized_by_requirements() -> std::io::Res
codex_home.path().join(CONFIG_TOML_FILE),
r#"
[features]
personality = false
view_image = false
shell_tool = true
"#,
)?;
@@ -11408,7 +11408,7 @@ shell_tool = true
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"
[features]
personality = true
view_image = true
shell_tool = false
"#,
),
@@ -11416,7 +11416,7 @@ shell_tool = false
.build()
.await?;
assert!(config.features.enabled(Feature::Personality));
assert!(config.features.enabled(Feature::ViewImage));
assert!(!config.features.enabled(Feature::ShellTool));
assert!(
!config
@@ -11430,6 +11430,55 @@ shell_tool = false
Ok(())
}
#[test]
fn retired_personality_feature_requirements_do_not_reject_configured_values() -> std::io::Result<()>
{
for (configured, required) in [(true, false), (false, true)] {
let cfg: ConfigToml = toml::from_str(&format!(
"[features]\npersonality = {configured}\nshell_tool = false\n"
))
.expect("valid config");
let requirement = Sourced::new(
FeatureRequirementsToml {
entries: BTreeMap::from([
("personality".to_string(), required),
("shell_tool".to_string(), false),
]),
},
RequirementSource::EnterpriseManaged {
id: "enterprise-id".to_string(),
name: "enterprise".to_string(),
},
);
validate_feature_requirements_for_config_toml(&cfg, Some(&requirement))?;
let configured_features = Features::from_sources(
FeatureConfigSource {
features: cfg.features.as_ref(),
..Default::default()
},
FeatureConfigSource::default(),
FeatureOverrides::default(),
);
let mut warnings = Vec::new();
let features = ManagedFeatures::from_configured_with_warnings(
configured_features,
Some(requirement),
&mut warnings,
)?;
assert_eq!(
(
features.enabled(Feature::Personality),
features.enabled(Feature::ShellTool),
warnings,
),
(false, false, Vec::new()),
);
}
Ok(())
}
#[tokio::test]
async fn approvals_reviewer_defaults_to_manual_only_without_guardian_feature() -> std::io::Result<()>
{
@@ -12327,7 +12376,7 @@ async fn feature_requirements_normalize_runtime_feature_mutations() -> std::io::
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"
[features]
personality = true
view_image = true
shell_tool = false
"#,
),
@@ -12337,7 +12386,7 @@ shell_tool = false
let mut requested = config.features.get().clone();
requested
.disable(Feature::Personality)
.disable(Feature::ViewImage)
.enable(Feature::ShellTool);
assert!(config.features.can_set(&requested).is_ok());
config
@@ -12345,7 +12394,7 @@ shell_tool = false
.set(requested)
.expect("managed feature mutations should normalize successfully");
assert!(config.features.enabled(Feature::Personality));
assert!(config.features.enabled(Feature::ViewImage));
assert!(!config.features.enabled(Feature::ShellTool));
Ok(())
@@ -218,6 +218,9 @@ fn parse_feature_requirements(
) -> BTreeMap<Feature, bool> {
let mut pinned_features = BTreeMap::new();
for (key, enabled) in feature_requirements.entries {
if key == Feature::Personality.key() {
continue;
}
if key == "auto_review" {
pinned_features.insert(Feature::GuardianApproval, enabled);
continue;
+2 -2
View File
@@ -640,7 +640,7 @@ pub struct Config {
/// Info needed to make an API request to the model.
pub model_provider: ModelProviderInfo,
/// Optionally specify the personality of the model
/// Deprecated: `friendly` and `pragmatic` no longer select a style.
pub personality: Option<Personality>,
/// Effective permission configuration for shell tool execution.
@@ -1622,7 +1622,6 @@ impl Config {
Some(BaseInstructionsProvenance::Model { .. })
)
}),
personality_enabled: self.features.enabled(Feature::Personality),
personality: self.personality,
model_catalog: self.model_catalog.clone(),
}
@@ -2575,6 +2574,7 @@ pub struct ConfigOverrides {
pub default_zsh_path: Option<AbsolutePathBuf>,
pub base_instructions: Option<String>,
pub developer_instructions: Option<String>,
/// Deprecated: `friendly` and `pragmatic` no longer select a style.
pub personality: Option<Personality>,
pub compact_prompt: Option<String>,
pub show_raw_agent_reasoning: Option<bool>,
@@ -374,7 +374,6 @@ impl Session {
&constraints,
self.services.models_manager.as_ref(),
&overrides,
self.features.enabled(Feature::Personality),
self.features.enabled(Feature::FastMode),
)
.await
@@ -513,10 +513,9 @@ async fn submitted_sparse_updates_preserve_captured_steps_and_ordering() {
let model_manager_config = {
let state = session.state.lock().await;
let configuration = &state.session_configuration;
configuration.model_info_overrides.models_manager_config(
configuration.step_settings.personality,
session.features.enabled(Feature::Personality),
)
configuration
.model_info_overrides
.models_manager_config(configuration.step_settings.personality)
};
let expected_destination = with_config_overrides(expected_destination, &model_manager_config);
let desired = desired_step_settings(&session).await;
+2 -10
View File
@@ -129,7 +129,6 @@ impl ResolvedStepSettings {
constraints: &StepSettingsConstraints<'_>,
models_manager: &dyn ModelsManager,
overrides: &ModelInfoOverrides,
personality_enabled: bool,
fast_mode_enabled: bool,
) -> ConstraintResult<Self> {
let selected = self.selected.apply(update, constraints)?;
@@ -139,11 +138,7 @@ impl ResolvedStepSettings {
{
Arc::clone(&self.model_info)
} else {
Arc::new(
selected
.resolve_model_info(models_manager, overrides, personality_enabled)
.await,
)
Arc::new(selected.resolve_model_info(models_manager, overrides).await)
};
let mut next = Self::new(Arc::new(selected), model_info, fast_mode_enabled);
next.mcp_approvals_reviewer_override = update
@@ -206,7 +201,6 @@ impl ModelInfoOverrides {
pub(crate) fn models_manager_config(
&self,
personality: Option<Personality>,
personality_enabled: bool,
) -> ModelsManagerConfig {
ModelsManagerConfig {
model_context_window: self.context_window,
@@ -214,7 +208,6 @@ impl ModelInfoOverrides {
tool_output_token_limit: self.tool_output_token_limit,
base_instructions: self.base_instructions.clone(),
personality,
personality_enabled,
// The models manager already owns its catalog.
model_catalog: None,
}
@@ -254,9 +247,8 @@ impl StepSettings {
&self,
models_manager: &dyn ModelsManager,
overrides: &ModelInfoOverrides,
personality_enabled: bool,
) -> ModelInfo {
let config = overrides.models_manager_config(self.personality, personality_enabled);
let config = overrides.models_manager_config(self.personality);
models_manager
.get_model_info(self.collaboration_mode.model(), &config)
.await
@@ -451,28 +451,14 @@ async fn model_resolution_preserves_startup_overrides_and_instruction_provenance
// Capture the same filtered explicit overrides that session startup owns.
let overrides = ModelInfoOverrides::from(config.to_models_manager_config());
for (personality, personality_enabled, catalog_instructions) in [
for (personality, catalog_instructions) in [
(
Personality::Friendly,
true,
"Catalog B.\n# Personality\nfixed\n# Rules\nKeep the rules.",
),
(
Personality::None,
true,
"Catalog B.\n# Rules\nKeep the rules.",
),
(
Personality::Friendly,
false,
"Catalog B.\n# Personality\nfixed\n# Rules\nKeep the rules.",
),
(Personality::None, "Catalog B.\n# Rules\nKeep the rules."),
] {
config.personality = Some(personality);
config
.features
.set_enabled(Feature::Personality, personality_enabled)
.expect("test config should allow personality changes");
let mut settings = configured_settings();
settings.collaboration_mode = settings.collaboration_mode.with_updates(
Some("model-b".to_string()),
@@ -485,11 +471,7 @@ async fn model_resolution_preserves_startup_overrides_and_instruction_provenance
.get_model_info("model-b", &config.to_models_manager_config())
.await;
let resolved = settings
.resolve_model_info(
&models_manager,
&overrides,
config.features.enabled(Feature::Personality),
)
.resolve_model_info(&models_manager, &overrides)
.await;
assert_eq!(resolved, legacy);
assert_eq!(
@@ -1018,7 +1018,6 @@ impl Session {
.resolve_model_info(
self.services.models_manager.as_ref(),
&session_configuration.model_info_overrides,
self.features.enabled(Feature::Personality),
)
.await;
let multi_agent_version = match multi_agent_runtime {
+3 -14
View File
@@ -181,14 +181,8 @@ async fn first_turn_model_change_appends_model_instructions_developer_message(
let server = MockServer::start().await;
let resp_mock = mount_sse_once(&server, sse_completed("resp-1")).await;
let mut builder = test_codex()
.with_model_info_override("gpt-5.6-terra", configure_model_switching_fixture)
.with_config(|config| {
config
.features
.enable(Feature::Personality)
.expect("test config should allow feature update");
});
let mut builder =
test_codex().with_model_info_override("gpt-5.6-terra", configure_model_switching_fixture);
let test = builder.build_with_auto_env(&server).await?;
let next_model = "gpt-5.5";
@@ -401,12 +395,7 @@ async fn model_change_with_legacy_personality_override_only_appends_model_instru
)
.await;
let mut builder = test_codex().with_model("gpt-5.5").with_config(|config| {
config
.features
.enable(Feature::Personality)
.expect("test config should allow feature update");
});
let mut builder = test_codex().with_model("gpt-5.5");
let test = builder.build(&server).await?;
let next_model = "exp-codex-personality";
@@ -549,10 +549,6 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
.with_config(|config| {
config.update_plan_enabled = true;
config.model = Some("gpt-5.4".to_string());
config
.features
.enable(Feature::Personality)
.expect("test config should allow feature update");
config.personality = Some(Personality::Pragmatic);
});
let resumed = resume_builder.restart(&server, &initial).await?;
+21 -29
View File
@@ -1,6 +1,5 @@
use codex_config::types::Personality;
use codex_core::TurnInputRequest;
use codex_features::Feature;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
@@ -64,10 +63,6 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> {
let server = start_mock_server().await;
let resp_mock = mount_sse_once(&server, sse_completed("resp-1")).await;
let mut builder = test_codex().with_model("gpt-5.5").with_config(|config| {
config
.features
.enable(Feature::Personality)
.expect("test config should allow feature update");
config.personality = Some(Personality::None);
});
let test = builder.build(&server).await?;
@@ -106,8 +101,13 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> {
Ok(())
}
#[test_case(None; "without feature config")]
#[test_case(Some(false); "with removed feature disabled")]
#[test_case(Some(true); "with removed feature enabled")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_personality_none_strips_baked_personality_section() -> anyhow::Result<()> {
async fn config_personality_none_strips_baked_personality_section(
legacy_feature_setting: Option<bool>,
) -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -119,14 +119,14 @@ async fn config_personality_none_strips_baked_personality_section() -> anyhow::R
model_messages.instructions_variables = None;
}
})
.with_config(|config| {
config
.features
.enable(Feature::Personality)
.expect("test config should allow feature update");
config.personality = Some(Personality::None);
.with_pre_build_hook(move |home| {
let mut config = "personality = \"none\"\n".to_string();
if let Some(value) = legacy_feature_setting {
config.push_str(&format!("[features]\npersonality = {value}\n"));
}
std::fs::write(home.join("config.toml"), config).expect("write personality config");
});
let test = builder.build(&server).await?;
let test = builder.build_with_auto_env(&server).await?;
test.codex
.start_or_steer_turn(read_only_text_turn(
@@ -152,7 +152,7 @@ async fn config_personality_none_strips_baked_personality_section() -> anyhow::R
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_personality_none_preserves_explicit_base_instructions(
custom_instructions: &'static str,
personality_enabled: bool,
legacy_feature_setting: bool,
) -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
@@ -160,12 +160,13 @@ async fn config_personality_none_preserves_explicit_base_instructions(
let resp_mock = mount_sse_once(&server, sse_completed("resp-1")).await;
let mut builder = test_codex()
.with_model("gpt-5.5")
.with_pre_build_hook(move |home| {
let config = format!(
"personality = \"none\"\n[features]\npersonality = {legacy_feature_setting}\n"
);
std::fs::write(home.join("config.toml"), config).expect("write personality config");
})
.with_config(move |config| {
config
.features
.set_enabled(Feature::Personality, personality_enabled)
.expect("test config should allow feature update");
config.personality = Some(Personality::None);
config.base_instructions = Some(custom_instructions.to_string());
});
let test = builder.build_with_auto_env(&server).await?;
@@ -198,12 +199,7 @@ async fn default_instructions_are_friendly_without_config_toml() -> anyhow::Resu
let server = start_mock_server().await;
let resp_mock = mount_sse_once(&server, sse_completed("resp-1")).await;
let mut builder = test_codex().with_model("gpt-5.5").with_config(|config| {
config
.features
.enable(Feature::Personality)
.expect("test config should allow feature update");
});
let mut builder = test_codex().with_model("gpt-5.5");
let test = builder.build(&server).await?;
assert_eq!(test.config.personality, None);
@@ -245,10 +241,6 @@ async fn fixed_friendly_personality_ignores_pragmatic_update(
)
.await;
let mut builder = test_codex().with_model(model).with_config(|config| {
config
.features
.enable(Feature::Personality)
.expect("test config should allow feature update");
config.personality = Some(Personality::Friendly);
});
let test = builder.build_with_auto_env(&server).await?;
+4 -4
View File
@@ -344,7 +344,7 @@ pub enum Feature {
AuthElicitation,
/// Offer Amazon Bedrock setup during TUI sign-in onboarding.
BedrockSetupWizard,
/// Enable personality selection in the TUI.
/// Removed compatibility flag retained as a no-op.
Personality,
/// Enable native artifact tools.
Artifact,
@@ -610,7 +610,7 @@ impl Features {
"image_detail_original" | "resize_all_images" | "item_ids" => {
continue;
}
"plugin_hooks" => {
"personality" | "plugin_hooks" => {
continue;
}
"skill_env_var_dependency_prompt" => {
@@ -1688,8 +1688,8 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::Personality,
key: "personality",
stage: Stage::Stable,
default_enabled: true,
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
id: Feature::Artifact,
+31 -2
View File
@@ -645,6 +645,35 @@ fn from_sources_ignores_removed_apply_patch_freeform_feature_key() {
assert_eq!(features, Features::with_defaults());
}
#[test]
fn from_sources_accepts_and_ignores_removed_personality_feature_values() {
for enabled in [false, true] {
let features_toml: FeaturesToml = toml::from_str(&format!("personality = {enabled}"))
.expect("legacy personality feature should deserialize");
let source = FeatureConfigSource {
features: Some(&features_toml),
..Default::default()
};
assert_eq!(
Features::from_sources(
source,
FeatureConfigSource::default(),
FeatureOverrides::default(),
),
Features::with_defaults()
);
assert_eq!(
Features::from_sources(
FeatureConfigSource::default(),
source,
FeatureOverrides::default(),
),
Features::with_defaults()
);
}
}
#[test]
fn from_sources_ignores_removed_plugin_hooks_feature_key() {
let features_toml = FeaturesToml::from(BTreeMap::from([("plugin_hooks".to_string(), true)]));
@@ -796,7 +825,7 @@ fn unstable_warning_event_only_mentions_enabled_under_development_features() {
"apply_patch_streaming_events".to_string(),
TomlValue::Boolean(true),
);
configured_features.insert("personality".to_string(), TomlValue::Boolean(true));
configured_features.insert("fast_mode".to_string(), TomlValue::Boolean(true));
configured_features.insert("unknown".to_string(), TomlValue::Boolean(true));
let mut features = Features::with_defaults();
@@ -814,7 +843,7 @@ fn unstable_warning_event_only_mentions_enabled_under_development_features() {
panic!("expected warning event");
};
assert!(message.contains("apply_patch_streaming_events"));
assert!(!message.contains("personality"));
assert!(!message.contains("fast_mode"));
assert!(message.contains("/tmp/config.toml"));
}
-1
View File
@@ -7,7 +7,6 @@ pub struct ModelsManagerConfig {
pub model_auto_compact_token_limit: Option<i64>,
pub tool_output_token_limit: Option<usize>,
pub base_instructions: Option<String>,
pub personality_enabled: bool,
pub personality: Option<Personality>,
pub model_catalog: Option<ModelsResponse>,
}
+1 -2
View File
@@ -60,8 +60,7 @@ pub fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig)
});
model_messages.instructions_template = Some(base_instructions.clone());
model_messages.instructions_variables = None;
} else if config.personality_enabled
&& config.personality == Some(Personality::None)
} else if config.personality == Some(Personality::None)
&& let Some(instructions_template) = model
.model_messages
.as_mut()
@@ -18,7 +18,6 @@ use pretty_assertions::assert_eq;
fn config_with_personality(personality: Option<Personality>) -> ModelsManagerConfig {
ModelsManagerConfig {
personality_enabled: true,
personality,
..Default::default()
}
@@ -26,7 +25,7 @@ fn config_with_personality(personality: Option<Personality>) -> ModelsManagerCon
#[test]
fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
let override_instructions = "override {{ personality }}";
let override_instructions = "override {{ personality }}\n# Personality\nKeep me";
let persistent_instructions = "Follow up on the active task.";
let async_message_description = "Catalog async message description.";
let mut model = model_info_from_slug("unknown-model");
@@ -104,6 +103,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
});
let config = ModelsManagerConfig {
base_instructions: Some(override_instructions.to_string()),
personality: Some(Personality::None),
..Default::default()
};
@@ -131,7 +131,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
})
);
assert_eq!(
updated.get_model_instructions(/*personality*/ None),
updated.get_model_instructions(Some(Personality::None)),
override_instructions
);
}
@@ -193,16 +193,12 @@ fn personality_none_strips_catalog_instruction_sources_through_the_next_h1() {
}
#[test]
fn baked_personality_section_is_preserved_without_enabled_explicit_none() {
fn baked_personality_section_is_preserved_without_explicit_none() {
let instructions = "Intro\n# Personality\nKeep me\n# General\nKeep me too";
let configs = [
config_with_personality(/*personality*/ None),
config_with_personality(Some(Personality::Friendly)),
config_with_personality(Some(Personality::Pragmatic)),
ModelsManagerConfig {
personality: Some(Personality::None),
..Default::default()
},
];
for config in configs {
@@ -220,6 +216,32 @@ fn baked_personality_section_is_preserved_without_enabled_explicit_none() {
}
}
#[test]
fn explicit_empty_base_instructions_stay_empty_with_personality_none() {
let mut model = model_info_from_slug("unknown-model");
model
.model_messages
.as_mut()
.expect("fallback model messages")
.instructions_template = Some("Intro\n# Personality\nRemove me".to_string());
let config = ModelsManagerConfig {
base_instructions: Some(String::new()),
personality: Some(Personality::None),
..Default::default()
};
let updated = with_config_overrides(model, &config);
assert_eq!(
updated
.model_messages
.as_ref()
.and_then(|messages| messages.instructions_template.as_deref()),
Some("")
);
assert_eq!(updated.get_model_instructions(config.personality), "");
}
#[test]
fn unknown_model_uses_builtin_instruction_template() {
let model = model_info_from_slug("unknown-model");
+1
View File
@@ -311,6 +311,7 @@ pub enum WindowsSandboxProxySettingsMode {
Preserve,
}
/// Deprecated: `friendly` and `pragmatic` no longer select a style.
#[derive(
Debug,
Serialize,
+26
View File
@@ -40,6 +40,7 @@ from openai_codex.types import (
CancelLoginAccountStatus,
GetAccountResponse,
InitializeResponse,
Personality,
ThreadItem,
ThreadTokenUsage,
TurnError,
@@ -88,6 +89,27 @@ server's default behavior. This does not remove history from the model's
context. Both methods return a thread handle; use `thread.read(include_turns=True)`
to retrieve its history.
### Deprecated personality selection
`thread_start(...)`, `thread_resume(...)`, and the thread's `run(...)` and
`turn(...)` still accept `personality` for compatibility. The current app-server
accepts `Personality.friendly` and `Personality.pragmatic`, but they no longer
select a style; model instructions define the tone.
Python `None` or omitting the option leaves it unset. Explicit `Personality.none`
(wire value `"none"`) strips the literal `# Personality` section the next time
Codex prepares instructions from the model catalog, such as when starting a
thread or switching models. It does not change explicitly supplied base
instructions or rewrite an existing thread's instructions when resuming or
starting a turn. Either legacy value can replace a previous `Personality.none`
setting for future model instructions. The old `features.personality` flag is
ignored.
Models returned by `models(...)` still expose `supports_personality` for
compatibility. This field is deprecated and always `False` on the current
app-server; it describes selectable personality, not the separate
`Personality.none` opt-out.
## AsyncCodex (async parity)
```python
@@ -121,6 +143,9 @@ Properties/methods:
- `thread_unarchive(thread_id: str) -> Awaitable[AsyncThread]`
- `models(*, include_hidden: bool = False) -> Awaitable[ModelListResponse]`
The [deprecated personality selection](#deprecated-personality-selection)
notes also apply to the async methods and model results.
Async context manager:
```python
@@ -198,6 +223,7 @@ These options have the same behavior on sync and async `run(...)` and `turn(...)
| Option | Behavior |
| --- | --- |
| `personality: Personality \| None = None` | `Personality.friendly` and `Personality.pragmatic` are deprecated and no longer select a style. See [deprecated personality selection](#deprecated-personality-selection). |
| `service_tier: str | None = None` | Sets the thread's service tier for this and subsequent turns. |
| `turn_service_tier: str | None = None` | Overrides the tier for a newly started turn only. `None` inherits the thread setting; `"default"` selects standard speed. Does not change the thread default and is ignored when input joins an active turn. |
| `source: str | None = None` | Labels the caller that initiated a new turn, such as `"review_ui"`. This is metadata; it does not schedule work or grant authority. Ignored when input joins an active turn. |
+10 -1
View File
@@ -297,7 +297,11 @@ class Codex:
# END GENERATED: Codex.flat_methods
def models(self, *, include_hidden: bool = False) -> ModelListResponse:
"""List available models reported by Codex."""
"""List available models reported by Codex.
The deprecated ``Model.supports_personality`` field is always ``False``
on the current app-server.
"""
return self._client.model_list(include_hidden=include_hidden)
@@ -557,6 +561,11 @@ class AsyncCodex:
# END GENERATED: AsyncCodex.flat_methods
async def models(self, *, include_hidden: bool = False) -> ModelListResponse:
"""List available models reported by Codex.
The deprecated ``Model.supports_personality`` field is always ``False``
on the current app-server.
"""
await self._ensure_initialized()
return await self._client.model_list(include_hidden=include_hidden)
@@ -5834,7 +5834,12 @@ class ThreadResumeParams(BaseModel):
str | None, Field(description="Configuration overrides for the resumed thread, if any.")
] = None
model_provider: Annotated[str | None, Field(alias="modelProvider")] = None
personality: Personality | None = None
personality: Annotated[
Personality | None,
Field(
description="@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
),
] = None
sandbox: SandboxMode | None = None
service_tier: Annotated[str | None, Field(alias="serviceTier")] = None
thread_id: Annotated[str, Field(alias="threadId")]
@@ -8521,7 +8526,13 @@ class Model(BaseModel):
supported_reasoning_efforts: Annotated[
list[ReasoningEffortOption], Field(alias="supportedReasoningEfforts")
]
supports_personality: Annotated[bool | None, Field(alias="supportsPersonality")] = False
supports_personality: Annotated[
bool | None,
Field(
alias="supportsPersonality",
description="@deprecated Always false; models no longer support personality selection.",
),
] = False
upgrade: str | None = None
upgrade_info: Annotated[ModelUpgradeInfo | None, Field(alias="upgradeInfo")] = None
@@ -9971,7 +9982,12 @@ class ThreadSettings(BaseModel):
effort: ReasoningEffort | None = None
model: str
model_provider: Annotated[str, Field(alias="modelProvider")]
personality: Personality | None = None
personality: Annotated[
Personality | None,
Field(
description="@deprecated Reports the saved setting; `friendly` and `pragmatic` no longer select a style."
),
] = None
sandbox_policy: Annotated[SandboxPolicy, Field(alias="sandboxPolicy")]
service_tier: Annotated[str | None, Field(alias="serviceTier")] = None
summary: ReasoningSummary | None = None
@@ -10004,7 +10020,10 @@ class ThreadStartParams(BaseModel):
ephemeral: bool | None = None
model: str | None = None
model_provider: Annotated[str | None, Field(alias="modelProvider")] = None
personality: Personality | None = None
personality: Annotated[
Personality | None,
Field(description="@deprecated `friendly` and `pragmatic` no longer select a style."),
] = None
sandbox: SandboxMode | None = None
service_name: Annotated[str | None, Field(alias="serviceName")] = None
service_tier: Annotated[str | None, Field(alias="serviceTier")] = None
@@ -10424,7 +10443,7 @@ class ConfigBatchWriteParams(BaseModel):
bool | None,
Field(
alias="reloadUserConfig",
description="When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults are not reloaded.",
description="When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, and service-tier defaults are not reloaded. The deprecated personality setting is also not reloaded.",
),
] = None
@@ -12141,7 +12160,9 @@ class TurnStartParams(BaseModel):
] = None
personality: Annotated[
Personality | None,
Field(description="Override the personality for this turn and subsequent turns."),
Field(
description="@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
),
] = None
sandbox_policy: Annotated[
SandboxPolicy | None,