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-06 21:10:19 -07:00
committed by GitHub
parent 4bb507d2c4
commit 24c598e8a9
39 changed files with 550 additions and 101 deletions

View File

@@ -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!({