Compare commits

...

1 Commits

Author SHA1 Message Date
Aismit Das
3ce2525848 Refresh AGENTS.md on cwd changes 2026-04-25 01:07:48 -04:00
4 changed files with 51 additions and 21 deletions

View File

@@ -7,6 +7,7 @@ use crate::context::PersonalitySpecInstructions;
use crate::context::RealtimeEndInstructions;
use crate::context::RealtimeStartInstructions;
use crate::context::RealtimeStartWithInstructions;
use crate::context::UserInstructions;
use crate::session::PreviousTurnSettings;
use crate::session::turn_context::TurnContext;
use crate::shell::Shell;
@@ -215,6 +216,17 @@ pub(crate) fn build_settings_update_items(
// inputs or add explicit replay events so fork/resume can diff everything
// deterministically.
let contextual_user_message = build_environment_update_item(previous, next, shell);
let user_instructions_message = if previous.and_then(|prev| prev.user_instructions.as_ref())
== next.user_instructions.as_ref()
{
None
} else {
let text = next.user_instructions.clone().unwrap_or_default();
Some(ContextualUserFragment::into(UserInstructions {
text,
directory: next.cwd.to_string_lossy().into_owned(),
}))
};
let developer_update_sections = [
// Keep model-switch instructions first so model-specific guidance is read before
// any other context diffs on this turn.
@@ -228,10 +240,13 @@ pub(crate) fn build_settings_update_items(
.flatten()
.collect();
let mut items = Vec::with_capacity(2);
let mut items = Vec::with_capacity(3);
if let Some(developer_message) = build_developer_update_item(developer_update_sections) {
items.push(developer_message);
}
if let Some(user_instructions_message) = user_instructions_message {
items.push(user_instructions_message);
}
if let Some(contextual_user_message) = contextual_user_message {
items.push(contextual_user_message);
}

View File

@@ -626,6 +626,9 @@ impl Session {
.map(|turn_environment| turn_environment.cwd.clone())
.unwrap_or_else(|| session_configuration.cwd.clone());
let per_turn_config = Self::build_per_turn_config(&session_configuration, cwd.clone());
let user_instructions = AgentsMdManager::new(&per_turn_config)
.user_instructions(environment.as_deref())
.await;
{
let mcp_connection_manager = self.services.mcp_connection_manager.read().await;
mcp_connection_manager.set_approval_policy(&session_configuration.approval_policy);
@@ -686,6 +689,7 @@ impl Session {
goal_tools_supported,
);
turn_context.realtime_active = self.conversation.running_state().await.is_some();
turn_context.user_instructions = user_instructions;
if let Some(final_schema) = final_output_json_schema {
turn_context.final_output_json_schema = final_schema;

View File

@@ -45,12 +45,12 @@ fn format_labeled_requests_snapshot(
)
}
fn user_instructions_wrapper_count(request: &ResponsesRequest) -> usize {
fn user_instructions_texts(request: &ResponsesRequest) -> Vec<String> {
request
.message_input_texts("user")
.iter()
.into_iter()
.filter(|text| text.starts_with("# AGENTS.md instructions for "))
.count()
.collect()
}
fn format_environment_context_subagents_snapshot(subagents: &[&str]) -> String {
@@ -181,9 +181,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
// TODO(ccunningham): Diff `user_instructions` and emit updates when AGENTS.md content changes
// (for example after cwd changes), then update this test to assert refreshed AGENTS content.
async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> Result<()> {
async fn snapshot_model_visible_layout_cwd_change_refreshes_agents() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -273,20 +271,28 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
let requests = responses.requests();
assert_eq!(requests.len(), 2, "expected two requests");
let first_agents = user_instructions_texts(&requests[0]);
assert_eq!(
user_instructions_wrapper_count(&requests[0]),
0,
"expected first request to omit the serialized user-instructions wrapper when cwd-only project docs are introduced after session init"
first_agents.len(),
1,
"expected first request to include agents_one AGENTS instructions"
);
assert!(first_agents[0].contains("Turn one agents instructions."));
let second_agents = user_instructions_texts(&requests[1]);
assert_eq!(
user_instructions_wrapper_count(&requests[1]),
0,
"expected second request to keep omitting the serialized user-instructions wrapper after cwd change with the current session-scoped project doc behavior"
second_agents.len(),
2,
"expected second request to include original and refreshed AGENTS instructions"
);
assert!(
second_agents
.last()
.is_some_and(|text| text.contains("Turn two agents instructions."))
);
insta::assert_snapshot!(
"model_visible_layout_cwd_change_does_not_refresh_agents",
"model_visible_layout_cwd_change_refreshes_agents",
format_labeled_requests_snapshot(
"Second turn changes cwd to a directory with different AGENTS.md; current behavior does not emit refreshed AGENTS instructions.",
"Second turn changes cwd to a directory with different AGENTS.md; refreshed AGENTS instructions are emitted.",
&[
("First Request (agents_one)", &requests[0]),
("Second Request (agents_two cwd)", &requests[1]),

View File

@@ -1,22 +1,27 @@
---
source: core/tests/suite/model_visible_layout.rs
expression: "format_labeled_requests_snapshot(\"Second turn changes cwd to a directory with different AGENTS.md; current behavior does not emit refreshed AGENTS instructions.\",\n&[(\"First Request (agents_one)\", &requests[0]),\n(\"Second Request (agents_two cwd)\", &requests[1]),])"
expression: "format_labeled_requests_snapshot(\"Second turn changes cwd to a directory with different AGENTS.md; refreshed AGENTS instructions are emitted.\",\n&[(\"First Request (agents_one)\", &requests[0]),\n(\"Second Request (agents_two cwd)\", &requests[1]),])"
---
Scenario: Second turn changes cwd to a directory with different AGENTS.md; current behavior does not emit refreshed AGENTS instructions.
Scenario: Second turn changes cwd to a directory with different AGENTS.md; refreshed AGENTS instructions are emitted.
## First Request (agents_one)
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <SKILLS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
01:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:first turn in agents_one
## Second Request (agents_two cwd)
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <SKILLS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
01:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:first turn in agents_one
03:message/assistant:turn one complete
04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
05:message/user:second turn in agents_two
04:message/user:<AGENTS_MD>
05:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
06:message/user:second turn in agents_two