Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/agent/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,9 @@ pub async fn handle_tool_calls<W: Write>(
if !approved {
let args_summary = super::display::format_args_summary(&call.function.arguments);
messages.push(Message {
role: Role::System,
role: Role::User,
content: format!(
"The user denied the '{}' tool call with arguments: {}\n\nTell the user you cannot proceed with that action unless they approve it.",
"[Tool denied] The user denied the '{}' tool call with arguments: {}\n\nTell the user you cannot proceed with that action unless they approve it.",
call.function.name, args_summary
),
tool_calls: vec![], images: vec![],
Expand Down
53 changes: 32 additions & 21 deletions src/commands/compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,16 @@ async fn compact_cascade(
Ok(token_usage)
}

/// Reconstruct the messages vector after compaction:
/// [system_prompt, summary_message, ...recent_messages]
/// Reconstruct the messages vector after compaction.
///
/// Merges the compaction summary into the existing system prompt rather than
/// creating a second `Role::System` message. Many models (e.g. Qwen 3.5) use
/// strict Jinja chat templates that raise errors like "System message must be
/// at the beginning" when they encounter more than one system message. By
/// appending the summary to the sole system prompt we stay compatible with
/// those models while still preserving the information.
///
/// Resulting layout: [system_prompt (with summary appended), ...recent_messages]
fn reconstruct_messages(
out: &mut Output,
messages: &mut Vec<Message>,
Expand All @@ -367,21 +375,24 @@ fn reconstruct_messages(
) {
let mut new_messages = Vec::new();

// Keep system prompt
// Merge the compaction summary into the system prompt so we keep exactly
// one `Role::System` message. This avoids "System message must be at the
// beginning" errors from models with strict Jinja chat templates (e.g.
// Qwen 3.5).
if let Some(sys) = ctx.system_msg.clone() {
new_messages.push(sys);
}

// Add the compaction summary as a system message
new_messages.push(Message {
role: Role::System,
content: format!(
"[Previous conversation summary]\n{}\n[End of summary — all details above have been compacted. \
let merged_content = format!(
"{}\n\n[Previous conversation summary]\n{}\n[End of summary — all details above have been compacted. \
If the user references something from before, it may be in this summary.]",
sys.content.trim_end(),
summary_content.trim()
),
tool_calls: vec![], images: vec![],
});
);
new_messages.push(Message {
role: Role::System,
content: merged_content,
tool_calls: sys.tool_calls,
images: sys.images,
});
}

// Keep all recent messages from keep_from onward
for msg in messages.drain(ctx.keep_from..) {
Expand Down Expand Up @@ -563,13 +574,13 @@ mod tests {
let mut out = Output::new(Box::new(Vec::new()));
reconstruct_messages(&mut out, &mut messages, &ctx, "This is a summary.");

// Should have: system + summary + 4 recent = 6 messages
assert_eq!(messages.len(), 6);
// Should have: merged system prompt (with summary) + 4 recent = 5 messages
assert_eq!(messages.len(), 5);
// The system prompt now contains both the original content and the summary
assert_eq!(messages[0].role, Role::System);
assert_eq!(messages[0].content, "You are helpful.");
assert_eq!(messages[1].role, Role::System);
assert!(messages[1].content.contains("This is a summary."));
assert_eq!(messages[2].content, "recent1");
assert_eq!(messages[5].content, "recent4");
assert!(messages[0].content.contains("You are helpful."));
assert!(messages[0].content.contains("This is a summary."));
assert_eq!(messages[1].content, "recent1");
assert_eq!(messages[4].content, "recent4");
}
}
Loading
Loading