Skip to content

Commit 71affef

Browse files
zenshanda-ai
andcommitted
fix: account for queued subagent content in compaction
Co-Authored-By: Anda Bot <noreply@anda.bot>
1 parent c38f311 commit 71affef

3 files changed

Lines changed: 69 additions & 42 deletions

File tree

anda_core/src/model.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,40 @@ impl ContentPart {
496496
Err(Box::new(self))
497497
}
498498
}
499+
500+
/// Estimates the number of tokens in this content part for usage accounting and pruning.
501+
pub fn estimated_tokens(&self) -> usize {
502+
match self {
503+
ContentPart::Text { text } | ContentPart::Reasoning { text } => estimate_tokens(text),
504+
ContentPart::FileData {
505+
file_uri,
506+
mime_type,
507+
} => estimate_tokens(file_uri)
508+
.saturating_add(mime_type.as_deref().map_or(0, estimate_tokens)),
509+
ContentPart::InlineData { mime_type, data } => {
510+
estimate_tokens(mime_type).saturating_add((data.len()).saturating_add(3) / 4)
511+
}
512+
ContentPart::ToolCall {
513+
name,
514+
args,
515+
call_id,
516+
} => estimate_tokens(name)
517+
.saturating_add(estimate_tokens(&args.to_string()))
518+
.saturating_add(call_id.as_deref().map_or(0, estimate_tokens)),
519+
ContentPart::ToolOutput {
520+
name,
521+
output,
522+
call_id,
523+
..
524+
} => estimate_tokens(name)
525+
.saturating_add(estimate_tokens(&output.to_string()))
526+
.saturating_add(call_id.as_deref().map_or(0, estimate_tokens)),
527+
ContentPart::Action { name, payload, .. } => {
528+
estimate_tokens(name).saturating_add(estimate_tokens(&payload.to_string()))
529+
}
530+
ContentPart::Any(value) => estimate_tokens(&value.to_string()),
531+
}
532+
}
499533
}
500534

501535
/// Converts a content part with inline data to a data URL string.
@@ -927,8 +961,8 @@ impl FunctionDefinition {
927961
}
928962

929963
/// Estimates token count using a small, provider-independent heuristic.
930-
pub fn estimate_tokens(content: &str) -> usize {
931-
content.len() / 3
964+
pub fn estimate_tokens(text: &str) -> usize {
965+
(text.chars().count()).saturating_add(3) / 4
932966
}
933967

934968
/// A document with metadata and content.

anda_engine/src/context/agent.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ impl AgentCtx {
257257
let lowercase = definition.name.to_ascii_lowercase();
258258
visible_names
259259
.insert(lowercase.clone())
260-
.then(|| (lowercase, definition.name))
260+
.then_some((lowercase, definition.name))
261261
})
262262
.collect();
263263
for group in self.agents.groups() {
@@ -1142,6 +1142,11 @@ impl CompletionRunner {
11421142
&& self.pending_tool_calls.is_empty()
11431143
}
11441144

1145+
/// Returns whether there are no pending tool calls.
1146+
pub fn no_pending_tool_calls(&self) -> bool {
1147+
self.pending_tool_calls.is_empty()
1148+
}
1149+
11451150
/// Returns the number of turns executed.
11461151
pub fn turns(&self) -> usize {
11471152
self.turns
@@ -1239,6 +1244,11 @@ impl CompletionRunner {
12391244
self.steering_message.extend(content);
12401245
}
12411246

1247+
/// Returns the iter over the queued steering message content parts.
1248+
pub fn steering_message_iter(&'_ self) -> core::slice::Iter<'_, ContentPart> {
1249+
self.steering_message.iter()
1250+
}
1251+
12421252
/// Queue a follow-up message for the next safe user turn.
12431253
/// Delivered with the current pending tool-call results when they finish, or at the next idle
12441254
/// boundary when no tools are pending. Steering still takes priority.
@@ -1258,6 +1268,11 @@ impl CompletionRunner {
12581268
self.follow_up_message.extend(content);
12591269
}
12601270

1271+
/// Returns the iter over the queued follow-up message content parts.
1272+
pub fn follow_up_message_iter(&'_ self) -> std::collections::vec_deque::Iter<'_, ContentPart> {
1273+
self.follow_up_message.iter()
1274+
}
1275+
12611276
/// Drops the current in-flight request after a transport-level model failure.
12621277
///
12631278
/// This keeps accumulated chat history, usage, artifacts, and queued follow-up messages, but

anda_engine/src/subagent.rs

Lines changed: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,9 @@ impl SubSessionRunner {
790790
/// own request, so it must run *before* the content is attached or the compaction request would
791791
/// overflow too.
792792
async fn compact_if_needed(&mut self, pending_tokens: u64) -> Result<(), BoxError> {
793-
if self.runner.is_idle() && needs_compaction_with_pending(&self.runner, pending_tokens) {
793+
if self.runner.no_pending_tool_calls()
794+
&& needs_compaction_with_pending(&self.runner, pending_tokens)
795+
{
794796
self.compact().await?;
795797
}
796798

@@ -897,7 +899,19 @@ impl SubSessionRunner {
897899
// queue it. Running unconditionally also covers the case where the committed history grew
898900
// over the threshold without any new input this round.
899901
let pending_tokens = estimated_content_tokens(&follow_up_batch)
900-
.saturating_add(estimated_content_tokens(&steer_batch));
902+
.saturating_add(estimated_content_tokens(&steer_batch))
903+
.saturating_add(
904+
self.runner
905+
.steering_message_iter()
906+
.map(|c| c.estimated_tokens() as u64)
907+
.sum(),
908+
)
909+
.saturating_add(
910+
self.runner
911+
.follow_up_message_iter()
912+
.map(|c| c.estimated_tokens() as u64)
913+
.sum(),
914+
);
901915
self.compact_if_needed(pending_tokens).await?;
902916
if !follow_up_batch.is_empty() {
903917
self.runner.follow_up_content(follow_up_batch);
@@ -2217,43 +2231,7 @@ fn estimated_message_tokens(message: &Message) -> u64 {
22172231
}
22182232

22192233
fn estimated_content_tokens(content: &[ContentPart]) -> u64 {
2220-
content.iter().map(estimated_content_part_tokens).sum()
2221-
}
2222-
2223-
fn estimated_content_part_tokens(content: &ContentPart) -> u64 {
2224-
match content {
2225-
ContentPart::Text { text } | ContentPart::Reasoning { text } => estimated_text_tokens(text),
2226-
ContentPart::FileData {
2227-
file_uri,
2228-
mime_type,
2229-
} => estimated_text_tokens(file_uri)
2230-
.saturating_add(mime_type.as_deref().map_or(0, estimated_text_tokens)),
2231-
ContentPart::InlineData { mime_type, data } => estimated_text_tokens(mime_type)
2232-
.saturating_add((data.len() as u64).saturating_add(3) / 4),
2233-
ContentPart::ToolCall {
2234-
name,
2235-
args,
2236-
call_id,
2237-
} => estimated_text_tokens(name)
2238-
.saturating_add(estimated_text_tokens(&args.to_string()))
2239-
.saturating_add(call_id.as_deref().map_or(0, estimated_text_tokens)),
2240-
ContentPart::ToolOutput {
2241-
name,
2242-
output,
2243-
call_id,
2244-
..
2245-
} => estimated_text_tokens(name)
2246-
.saturating_add(estimated_text_tokens(&output.to_string()))
2247-
.saturating_add(call_id.as_deref().map_or(0, estimated_text_tokens)),
2248-
ContentPart::Action { name, payload, .. } => {
2249-
estimated_text_tokens(name).saturating_add(estimated_text_tokens(&payload.to_string()))
2250-
}
2251-
ContentPart::Any(value) => estimated_text_tokens(&value.to_string()),
2252-
}
2253-
}
2254-
2255-
fn estimated_text_tokens(text: &str) -> u64 {
2256-
(text.chars().count() as u64).saturating_add(3) / 4
2234+
content.iter().map(|c| c.estimated_tokens() as u64).sum()
22572235
}
22582236

22592237
#[cfg(test)]

0 commit comments

Comments
 (0)