Improve agent retries and tool progress
This commit is contained in:
@@ -353,6 +353,9 @@ pub enum ProviderRunProtocolError {
|
||||
InvalidDriverObservation {
|
||||
message: String,
|
||||
},
|
||||
InvalidTranscriptCompaction {
|
||||
message: String,
|
||||
},
|
||||
InvalidRestoredState {
|
||||
message: String,
|
||||
},
|
||||
@@ -415,6 +418,9 @@ impl fmt::Display for ProviderRunProtocolError {
|
||||
Self::InvalidDriverObservation { message } => {
|
||||
write!(f, "invalid driver observation: {message}")
|
||||
}
|
||||
Self::InvalidTranscriptCompaction { message } => {
|
||||
write!(f, "invalid transcript compaction: {message}")
|
||||
}
|
||||
Self::InvalidRestoredState { message } => {
|
||||
write!(f, "invalid restored provider run: {message}")
|
||||
}
|
||||
@@ -490,6 +496,36 @@ impl ProviderRun {
|
||||
self.tool_result_archive = archive;
|
||||
}
|
||||
|
||||
/// Replaces a summarized prefix while the run is parked between model calls.
|
||||
///
|
||||
/// Tool calls and results removed from the live transcript remain available to
|
||||
/// `recall_tool_history` through the run-owned archive.
|
||||
pub fn compact_transcript_at_model_boundary(
|
||||
&mut self,
|
||||
summarized_messages: usize,
|
||||
summary_prefix: Vec<ConversationMessage>,
|
||||
) -> Result<(), ProviderRunProtocolError> {
|
||||
if !matches!(self.state, ProviderRunState::ReadyToCallModel) {
|
||||
return Err(self.unexpected_state(ProviderRunPhase::ReadyToCallModel));
|
||||
}
|
||||
if summarized_messages == 0 || summarized_messages > self.transcript.len() {
|
||||
return Err(ProviderRunProtocolError::InvalidTranscriptCompaction {
|
||||
message: format!(
|
||||
"cannot summarize {summarized_messages} of {} messages",
|
||||
self.transcript.len()
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let drained = self
|
||||
.transcript
|
||||
.drain(0..summarized_messages)
|
||||
.collect::<Vec<_>>();
|
||||
archive_tool_results(&mut self.tool_result_archive, drained);
|
||||
self.transcript.splice(0..0, summary_prefix);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn usage(&self) -> &Usage {
|
||||
&self.usage
|
||||
}
|
||||
@@ -1662,6 +1698,48 @@ fn add_usage(total: &mut Usage, turn: &Usage) {
|
||||
.saturating_add(turn.cache_creation_input_tokens);
|
||||
}
|
||||
|
||||
fn archive_tool_results(
|
||||
archive: &mut Vec<ConversationMessage>,
|
||||
messages: Vec<ConversationMessage>,
|
||||
) {
|
||||
const MAX_TOOL_RESULT_ARCHIVE_ENTRIES: usize = 400;
|
||||
|
||||
let mut pending_tool_uses = Vec::new();
|
||||
for message in messages {
|
||||
match &message.content {
|
||||
MessageContent::ToolUse { .. } => pending_tool_uses.push(message),
|
||||
MessageContent::ToolResult { .. } => {
|
||||
if let Some(tool_use) = pending_tool_uses.pop() {
|
||||
archive.push(tool_use);
|
||||
}
|
||||
archive.push(message);
|
||||
}
|
||||
MessageContent::MultiPart(parts) => {
|
||||
for part in parts {
|
||||
match part {
|
||||
ContentPart::ToolUse { .. } => pending_tool_uses.push(message.clone()),
|
||||
ContentPart::ToolResult { .. } => {
|
||||
if let Some(tool_use) = pending_tool_uses.pop() {
|
||||
archive.push(tool_use);
|
||||
}
|
||||
archive.push(message.clone());
|
||||
}
|
||||
ContentPart::Text(_)
|
||||
| ContentPart::Reasoning { .. }
|
||||
| ContentPart::Image { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
MessageContent::Text(_) => {}
|
||||
}
|
||||
}
|
||||
archive.extend(pending_tool_uses);
|
||||
|
||||
if archive.len() > MAX_TOOL_RESULT_ARCHIVE_ENTRIES {
|
||||
archive.drain(0..archive.len() - MAX_TOOL_RESULT_ARCHIVE_ENTRIES);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "provider_run_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1211,6 +1211,60 @@ fn recovery_pending_tool_survives_another_restore_and_completes_once() {
|
||||
assert_eq!(restored.state().phase(), ProviderRunPhase::ReadyToCallModel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transcript_compaction_at_model_boundary_archives_drained_tool_history() {
|
||||
let mut run = run();
|
||||
let batch = accept_tool_turn(
|
||||
&mut run,
|
||||
tool_turn(vec![tool_call("read", "read_files")], &["read_files"]),
|
||||
);
|
||||
run.complete_tool(
|
||||
&batch.work_id,
|
||||
successful_result("read", "important file contents"),
|
||||
)
|
||||
.unwrap();
|
||||
run.commit_tool_batch(&batch.work_id).unwrap();
|
||||
let original_len = run.transcript().len();
|
||||
let summary_prefix = vec![ConversationMessage {
|
||||
role: MessageRole::User,
|
||||
content: MessageContent::Text("summary".to_string()),
|
||||
}];
|
||||
|
||||
run.compact_transcript_at_model_boundary(original_len, summary_prefix.clone())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(run.transcript(), summary_prefix);
|
||||
assert_eq!(run.tool_result_archive().len(), 2);
|
||||
assert!(matches!(
|
||||
run.tool_result_archive()[0].content,
|
||||
MessageContent::MultiPart(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
run.tool_result_archive()[1].content,
|
||||
MessageContent::MultiPart(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transcript_compaction_is_rejected_during_a_model_call() {
|
||||
let mut run = run();
|
||||
let _ = next_model_call(&mut run);
|
||||
|
||||
assert!(matches!(
|
||||
run.compact_transcript_at_model_boundary(
|
||||
1,
|
||||
vec![ConversationMessage {
|
||||
role: MessageRole::User,
|
||||
content: MessageContent::Text("summary".to_string()),
|
||||
}],
|
||||
),
|
||||
Err(ProviderRunProtocolError::UnexpectedState {
|
||||
expected: ProviderRunPhase::ReadyToCallModel,
|
||||
actual: ProviderRunPhase::AwaitingModel,
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_normalization_commits_a_fully_resolved_batch() {
|
||||
let mut run = run();
|
||||
|
||||
Reference in New Issue
Block a user