Cache-safe request assembly: append-only turn context and declared cache semantics (#11022)
This commit is contained in:
@@ -248,6 +248,20 @@ pub async fn handle_term_log(command: String) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn shell_history_text(newest_first_tail: &[&Message]) -> Option<String> {
|
||||
let history: Vec<String> = newest_first_tail
|
||||
.iter()
|
||||
.filter(|m| !m.is_turn_context())
|
||||
.rev()
|
||||
.map(|m| m.as_concat_text())
|
||||
.collect();
|
||||
if history.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(history.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_term_run(prompt: Vec<String>) -> Result<()> {
|
||||
let prompt = prompt.join(" ");
|
||||
let session_id = std::env::var("AGENT_SESSION_ID").map_err(|_| {
|
||||
@@ -291,20 +305,12 @@ pub async fn handle_term_run(prompt: Vec<String>) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let prompt_with_context = if user_messages_after_last_assistant.is_empty() {
|
||||
prompt
|
||||
} else {
|
||||
let history = user_messages_after_last_assistant
|
||||
.iter()
|
||||
.rev() // back to chronological order
|
||||
.map(|m| m.as_concat_text())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
format!(
|
||||
let prompt_with_context = match shell_history_text(&user_messages_after_last_assistant) {
|
||||
Some(history) => format!(
|
||||
"<shell_history>\n{}\n</shell_history>\n\n{}",
|
||||
history, prompt
|
||||
)
|
||||
),
|
||||
None => prompt,
|
||||
};
|
||||
|
||||
let config = SessionBuilderConfig {
|
||||
@@ -407,4 +413,24 @@ mod tests {
|
||||
|
||||
assert!(!script.contains("command_not_found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_history_skips_turn_context_events() {
|
||||
use goose::conversation::message::MessageMetadata;
|
||||
|
||||
let older = Message::user().with_text("git status");
|
||||
let block = Message::user()
|
||||
.with_text("<turn-context>cwd /repo</turn-context>")
|
||||
.with_metadata(MessageMetadata::agent_only().with_turn_context());
|
||||
let newer = Message::user().with_text("cargo build");
|
||||
let newest_first_tail = vec![&newer, &block, &older];
|
||||
|
||||
assert_eq!(
|
||||
shell_history_text(&newest_first_tail).unwrap(),
|
||||
"git status\ncargo build"
|
||||
);
|
||||
|
||||
let only_block = vec![&block];
|
||||
assert_eq!(shell_history_text(&only_block), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user