Cache-safe request assembly: append-only turn context and declared cache semantics (#11022)

This commit is contained in:
filip
2026-08-06 20:08:49 -07:00
committed by GitHub
parent ca52cce628
commit 66051ec7d2
39 changed files with 1589 additions and 1231 deletions
+60 -21
View File
@@ -12,7 +12,7 @@ mod thinking;
use crate::session::task_execution_display::{
format_task_execution_notification, TASK_EXECUTION_NOTIFICATION_TYPE,
};
use goose::conversation::{fix_conversation, Conversation};
use goose::conversation::{fix_conversation, merge_consecutive_messages_for_request, Conversation};
use std::env;
use std::io::Write;
use std::str::FromStr;
@@ -69,8 +69,16 @@ const SHELL_STATUS_MAX_LINES: usize = 3;
const SHELL_STATUS_RESERVED_WIDTH: usize = 2;
fn planner_provider_messages(plan_messages: &Conversation) -> Conversation {
let projected_messages = plan_messages.agent_visible_messages();
fix_conversation(Conversation::new_unvalidated(projected_messages)).0
// The planner prompt has no turn-context instructions; drop the blocks.
let projected_messages: Vec<Message> = plan_messages
.agent_visible_messages()
.into_iter()
.filter(|message| !message.is_turn_context())
.collect();
let fixed = fix_conversation(Conversation::new_unvalidated(projected_messages)).0;
Conversation::new_unvalidated(merge_consecutive_messages_for_request(
fixed.messages().clone(),
))
}
#[derive(Serialize, Deserialize, Debug)]
@@ -1645,24 +1653,29 @@ impl CliSession {
&Message::assistant().with_text(interrupt_prompt),
self.debug,
);
} else if let Some(last_msg) = self.messages.last() {
if last_msg.role == rmcp::model::Role::User {
match last_msg.content.first() {
Some(MessageContent::ToolResponse(_)) => {
self.push_message(Message::assistant().with_text(interrupt_prompt));
output::render_message(
&Message::assistant().with_text(interrupt_prompt),
self.debug,
);
}
Some(_) => {
self.messages.pop();
let assistant_msg = Message::assistant().with_text(interrupt_prompt);
self.push_message(assistant_msg.clone());
output::render_message(&assistant_msg, self.debug);
}
None => {
// Empty message content — nothing to do, just continue gracefully
} else {
while self.messages.last().is_some_and(Message::is_turn_context) {
self.messages.pop();
}
if let Some(last_msg) = self.messages.last() {
if last_msg.role == rmcp::model::Role::User {
match last_msg.content.first() {
Some(MessageContent::ToolResponse(_)) => {
self.push_message(Message::assistant().with_text(interrupt_prompt));
output::render_message(
&Message::assistant().with_text(interrupt_prompt),
self.debug,
);
}
Some(_) => {
self.messages.pop();
let assistant_msg = Message::assistant().with_text(interrupt_prompt);
self.push_message(assistant_msg.clone());
output::render_message(&assistant_msg, self.debug);
}
None => {
// Empty message content — nothing to do, just continue gracefully
}
}
}
}
@@ -2646,6 +2659,32 @@ mod tests {
.contains("hidden separator"));
}
#[test]
fn planner_history_excludes_turn_context_events() {
use goose::conversation::message::MessageMetadata;
let history = Conversation::new_unvalidated([
Message::user().with_text("plan the refactor"),
Message::user()
.with_text("<turn-context>cwd /repo, todo: ship v2</turn-context>")
.with_metadata(MessageMetadata::agent_only().with_turn_context()),
Message::assistant().with_text("on it"),
]);
let provider_text = planner_provider_messages(&history)
.agent_visible_messages()
.iter()
.map(|message| message.as_concat_text())
.collect::<Vec<_>>()
.join("\n");
assert!(provider_text.contains("plan the refactor"));
assert!(
!provider_text.contains("turn-context"),
"the planner prompt has no turn-context instructions, so blocks must not reach it"
);
}
#[test]
fn test_format_elapsed_time_under_60_seconds() {
// Test sub-second duration