feat(core) update Personality on turn (#9644)

## Summary
Support updating Personality mid-Thread via UserTurn/OverwriteTurn. This
is explicitly unused by the clients so far, to simplify PRs - app-server
and tui implementations will be follow-ups.

## Testing
- [x] added integration tests
This commit is contained in:
Dylan Hurd
2026-01-22 12:04:23 -08:00
committed by GitHub
parent 4210fb9e6c
commit 8b3521ee77
42 changed files with 604 additions and 4 deletions
+56 -1
View File
@@ -166,6 +166,7 @@ use crate::util::backoff;
use codex_async_utils::OrCancelExt;
use codex_otel::OtelManager;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::models::ContentItem;
use codex_protocol::models::DeveloperInstructions;
@@ -301,6 +302,7 @@ impl Codex {
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions,
personality: config.model_personality,
base_instructions,
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy.clone(),
@@ -415,6 +417,7 @@ pub(crate) struct TurnContext {
pub(crate) developer_instructions: Option<String>,
pub(crate) compact_prompt: Option<String>,
pub(crate) user_instructions: Option<String>,
pub(crate) personality: Option<Personality>,
pub(crate) approval_policy: AskForApproval,
pub(crate) sandbox_policy: SandboxPolicy,
pub(crate) shell_environment_policy: ShellEnvironmentPolicy,
@@ -454,6 +457,9 @@ pub(crate) struct SessionConfiguration {
/// Model instructions that are appended to the base instructions.
user_instructions: Option<String>,
/// Personality preference for the model.
personality: Option<Personality>,
/// Base instructions for the session.
base_instructions: String,
@@ -489,6 +495,9 @@ impl SessionConfiguration {
if let Some(summary) = updates.reasoning_summary {
next_configuration.model_reasoning_summary = summary;
}
if let Some(personality) = updates.personality {
next_configuration.personality = Some(personality);
}
if let Some(approval_policy) = updates.approval_policy {
next_configuration.approval_policy.set(approval_policy)?;
}
@@ -510,6 +519,7 @@ pub(crate) struct SessionSettingsUpdate {
pub(crate) collaboration_mode: Option<CollaborationMode>,
pub(crate) reasoning_summary: Option<ReasoningSummaryConfig>,
pub(crate) final_output_json_schema: Option<Option<Value>>,
pub(crate) personality: Option<Personality>,
}
impl Session {
@@ -521,6 +531,7 @@ impl Session {
per_turn_config.model_reasoning_effort =
session_configuration.collaboration_mode.reasoning_effort();
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
per_turn_config.model_personality = session_configuration.personality;
per_turn_config.features = config.features.clone();
per_turn_config
}
@@ -566,6 +577,7 @@ impl Session {
developer_instructions: session_configuration.developer_instructions.clone(),
compact_prompt: session_configuration.compact_prompt.clone(),
user_instructions: session_configuration.user_instructions.clone(),
personality: session_configuration.personality,
approval_policy: session_configuration.approval_policy.value(),
sandbox_policy: session_configuration.sandbox_policy.get().clone(),
shell_environment_policy: per_turn_config.shell_environment_policy.clone(),
@@ -1109,6 +1121,34 @@ impl Session {
)
}
fn build_personality_update_item(
&self,
previous: Option<&Arc<TurnContext>>,
next: &TurnContext,
) -> Option<ResponseItem> {
let personality = next.personality?;
if let Some(prev) = previous
&& prev.personality == Some(personality)
{
return None;
}
let model_info = next.client.get_model_info();
let personality_message = Self::personality_message_for(&model_info, personality);
personality_message.map(|personality_message| {
DeveloperInstructions::personality_spec_message(personality_message).into()
})
}
fn personality_message_for(model_info: &ModelInfo, personality: Personality) -> Option<String> {
model_info
.model_instructions_template
.as_ref()
.and_then(|template| template.personality_messages.as_ref())
.and_then(|messages| messages.0.get(&personality))
.cloned()
}
fn build_collaboration_mode_update_item(
&self,
previous_collaboration_mode: &CollaborationMode,
@@ -1150,6 +1190,11 @@ impl Session {
) {
update_items.push(collaboration_mode_item);
}
if let Some(personality_item) =
self.build_personality_update_item(previous_context, current_context)
{
update_items.push(personality_item);
}
update_items
}
@@ -2014,6 +2059,7 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
effort,
summary,
collaboration_mode,
personality,
} => {
let collaboration_mode = if let Some(collab_mode) = collaboration_mode {
collab_mode
@@ -2034,6 +2080,7 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
sandbox_policy,
collaboration_mode: Some(collaboration_mode),
reasoning_summary: summary,
personality,
..Default::default()
},
)
@@ -2218,6 +2265,7 @@ mod handlers {
final_output_json_schema,
items,
collaboration_mode,
personality,
} => {
let collaboration_mode = collaboration_mode.or_else(|| {
Some(CollaborationMode::Custom(Settings {
@@ -2235,6 +2283,7 @@ mod handlers {
collaboration_mode,
reasoning_summary: Some(summary),
final_output_json_schema: Some(final_output_json_schema),
personality,
},
)
}
@@ -2708,6 +2757,7 @@ async fn spawn_review_thread(
developer_instructions: None,
user_instructions: None,
compact_prompt: parent_turn_context.compact_prompt.clone(),
personality: parent_turn_context.personality,
approval_policy: parent_turn_context.approval_policy,
sandbox_policy: parent_turn_context.sandbox_policy.clone(),
shell_environment_policy: parent_turn_context.shell_environment_policy.clone(),
@@ -2997,7 +3047,7 @@ async fn run_sampling_request(
tools: router.specs(),
parallel_tool_calls: model_supports_parallel,
base_instructions,
personality: None,
personality: turn_context.personality,
output_schema: turn_context.final_output_json_schema.clone(),
};
@@ -3116,6 +3166,7 @@ async fn try_run_sampling_request(
approval_policy: turn_context.approval_policy,
sandbox_policy: turn_context.sandbox_policy.clone(),
model: turn_context.client.get_model(),
personality: turn_context.personality,
collaboration_mode: Some(collaboration_mode),
effort: turn_context.client.get_reasoning_effort(),
summary: turn_context.client.get_reasoning_summary(),
@@ -3748,6 +3799,7 @@ mod tests {
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
personality: config.model_personality,
base_instructions: config
.base_instructions
.clone()
@@ -3823,6 +3875,7 @@ mod tests {
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
personality: config.model_personality,
base_instructions: config
.base_instructions
.clone()
@@ -4082,6 +4135,7 @@ mod tests {
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
personality: config.model_personality,
base_instructions: config
.base_instructions
.clone()
@@ -4186,6 +4240,7 @@ mod tests {
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
personality: config.model_personality,
base_instructions: config
.base_instructions
.clone()
+2
View File
@@ -94,6 +94,7 @@ async fn run_compact_task_inner(
approval_policy: turn_context.approval_policy,
sandbox_policy: turn_context.sandbox_policy.clone(),
model: turn_context.client.get_model(),
personality: turn_context.personality,
collaboration_mode: Some(collaboration_mode),
effort: turn_context.client.get_reasoning_effort(),
summary: turn_context.client.get_reasoning_summary(),
@@ -111,6 +112,7 @@ async fn run_compact_task_inner(
let prompt = Prompt {
input: turn_input,
base_instructions: sess.get_base_instructions().await,
personality: turn_context.personality,
..Default::default()
};
let attempt_result = drain_to_completed(&sess, turn_context.as_ref(), &prompt).await;
+1 -1
View File
@@ -55,7 +55,7 @@ async fn run_remote_compact_task_inner_impl(
tools: vec![],
parallel_tool_calls: false,
base_instructions: sess.get_base_instructions().await,
personality: None,
personality: turn_context.personality,
output_schema: None,
};
+3 -1
View File
@@ -86,7 +86,9 @@ impl ContextManager {
// This is a coarse lower bound, not a tokenizer-accurate count.
pub(crate) fn estimate_token_count(&self, turn_context: &TurnContext) -> Option<i64> {
let model_info = turn_context.client.get_model_info();
let personality = turn_context.client.config().model_personality;
let personality = turn_context
.personality
.or(turn_context.client.config().model_personality);
let base_instructions = model_info.get_model_instructions(personality);
let base_tokens = i64::try_from(approx_token_count(&base_instructions)).unwrap_or(i64::MAX);