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
@@ -535,7 +535,7 @@ async fn build_runner_options(
let max_concurrency =
normalize_concurrency(requested_concurrency, turn.config.agent_max_threads);
let base_instructions = session.get_base_instructions().await;
let spawn_config = build_agent_spawn_config(&base_instructions, turn.as_ref())?;
let spawn_config = build_agent_spawn_config(base_instructions.as_ref(), turn.as_ref())?;
Ok(JobRunnerOptions {
max_concurrency,
spawn_config,
@@ -59,8 +59,10 @@ impl ToolHandler for Handler {
.into(),
)
.await;
let mut config =
build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?;
let mut config = build_agent_spawn_config(
session.get_base_instructions().await.as_ref(),
turn.as_ref(),
)?;
apply_requested_spawn_agent_model_overrides(
&session,
turn.as_ref(),
@@ -201,11 +201,12 @@ pub(crate) fn parse_collab_input(
/// skipping this helper and cloning stale config state directly can send the child agent out with
/// the wrong provider or runtime policy.
pub(crate) fn build_agent_spawn_config(
base_instructions: &BaseInstructions,
base_instructions: Option<&BaseInstructions>,
turn: &TurnContext,
) -> Result<Config, FunctionCallError> {
let mut config = build_agent_shared_config(turn)?;
config.base_instructions = Some(base_instructions.text.clone());
config.base_instructions =
Some(base_instructions.map(|base_instructions| base_instructions.text.clone()));
Ok(config)
}
@@ -3209,9 +3209,9 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
.set(AskForApproval::OnRequest)
.expect("approval policy set");
let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config");
let config = build_agent_spawn_config(Some(&base_instructions), &turn).expect("spawn config");
let mut expected = (*turn.config).clone();
expected.base_instructions = Some(base_instructions.text);
expected.base_instructions = Some(Some(base_instructions.text));
expected.model = Some(turn.model_info.slug.clone());
expected.model_provider = turn.provider.clone();
expected.model_reasoning_effort = turn.reasoning_effort;
@@ -3247,7 +3247,7 @@ async fn build_agent_spawn_config_preserves_base_user_instructions() {
text: "base".to_string(),
};
let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config");
let config = build_agent_spawn_config(Some(&base_instructions), &turn).expect("spawn config");
assert_eq!(config.user_instructions, base_config.user_instructions);
}
@@ -3256,7 +3256,7 @@ async fn build_agent_spawn_config_preserves_base_user_instructions() {
async fn build_agent_resume_config_clears_base_instructions() {
let (_session, mut turn) = make_session_and_context().await;
let mut base_config = (*turn.config).clone();
base_config.base_instructions = Some("caller-base".to_string());
base_config.base_instructions = Some(Some("caller-base".to_string()));
turn.config = Arc::new(base_config);
turn.approval_policy
.set(AskForApproval::OnRequest)
@@ -68,8 +68,10 @@ impl ToolHandler for Handler {
.into(),
)
.await;
let mut config =
build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?;
let mut config = build_agent_spawn_config(
session.get_base_instructions().await.as_ref(),
turn.as_ref(),
)?;
apply_requested_spawn_agent_model_overrides(
&session,
turn.as_ref(),