Honor null thread instructions (#16964)

- Treat explicit null thread instructions as a blank-slate override
while preserving omitted-field fallback behavior.
- Preserve null through rollout resume/fork and keep explicit empty
strings distinct.
- Add app-server v2 start/fork coverage for the tri-state instruction
params.
This commit is contained in:
Ahmed Ibrahim
2026-04-07 04:10:19 +00:00
committed by GitHub
parent 4bb507d2c4
commit 24c598e8a9
39 changed files with 550 additions and 101 deletions
@@ -27,7 +27,7 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier
* 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, ephemeral?: boolean, /**
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null | null, developerInstructions?: string | null | null, ephemeral?: boolean, /**
* If true, persist additional rollout EventMsg variants required to
* reconstruct a richer thread history on subsequent resume/fork/read.
*/
@@ -36,7 +36,7 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier
* 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 | null, developerInstructions?: string | null | null, personality?: Personality | null, /**
* If true, persist additional rollout EventMsg variants required to
* reconstruct a richer thread history on subsequent resume/fork/read.
*/
@@ -12,7 +12,7 @@ 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, /**
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null | null, developerInstructions?: string | null | null, personality?: Personality | null, ephemeral?: boolean | null, /**
* If true, opt into emitting raw Responses API items on the event stream.
* This is for internal use only (e.g. Codex Cloud).
*/
@@ -2603,10 +2603,22 @@ pub struct ThreadStartParams {
pub config: Option<HashMap<String, JsonValue>>,
#[ts(optional = nullable)]
pub service_name: Option<String>,
#[serde(
default,
deserialize_with = "super::serde_helpers::deserialize_double_option",
serialize_with = "super::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub base_instructions: Option<String>,
pub base_instructions: Option<Option<String>>,
#[serde(
default,
deserialize_with = "super::serde_helpers::deserialize_double_option",
serialize_with = "super::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub developer_instructions: Option<String>,
pub developer_instructions: Option<Option<String>>,
#[ts(optional = nullable)]
pub personality: Option<Personality>,
#[ts(optional = nullable)]
@@ -2721,10 +2733,22 @@ pub struct ThreadResumeParams {
pub sandbox: Option<SandboxMode>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, serde_json::Value>>,
#[serde(
default,
deserialize_with = "super::serde_helpers::deserialize_double_option",
serialize_with = "super::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub base_instructions: Option<String>,
pub base_instructions: Option<Option<String>>,
#[serde(
default,
deserialize_with = "super::serde_helpers::deserialize_double_option",
serialize_with = "super::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub developer_instructions: Option<String>,
pub developer_instructions: Option<Option<String>>,
#[ts(optional = nullable)]
pub personality: Option<Personality>,
/// If true, persist additional rollout EventMsg variants required to
@@ -2798,10 +2822,22 @@ pub struct ThreadForkParams {
pub sandbox: Option<SandboxMode>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, serde_json::Value>>,
#[serde(
default,
deserialize_with = "super::serde_helpers::deserialize_double_option",
serialize_with = "super::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub base_instructions: Option<String>,
pub base_instructions: Option<Option<String>>,
#[serde(
default,
deserialize_with = "super::serde_helpers::deserialize_double_option",
serialize_with = "super::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub developer_instructions: Option<String>,
pub developer_instructions: Option<Option<String>>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub ephemeral: bool,
/// If true, persist additional rollout EventMsg variants required to
@@ -8325,6 +8361,35 @@ mod tests {
assert_eq!(serialized_without_override.get("serviceTier"), None);
}
#[test]
fn thread_start_params_preserve_explicit_null_instructions() {
let params: ThreadStartParams = serde_json::from_value(json!({
"baseInstructions": null,
"developerInstructions": null,
}))
.expect("params should deserialize");
assert_eq!(params.base_instructions, Some(None));
assert_eq!(params.developer_instructions, Some(None));
let serialized = serde_json::to_value(&params).expect("params should serialize");
assert_eq!(
serialized.get("baseInstructions"),
Some(&serde_json::Value::Null)
);
assert_eq!(
serialized.get("developerInstructions"),
Some(&serde_json::Value::Null)
);
let serialized_without_override =
serde_json::to_value(ThreadStartParams::default()).expect("params should serialize");
assert_eq!(serialized_without_override.get("baseInstructions"), None);
assert_eq!(
serialized_without_override.get("developerInstructions"),
None
);
}
#[test]
fn turn_start_params_preserve_explicit_null_service_tier() {
let params: TurnStartParams = serde_json::from_value(json!({