Bound agent context and preprocess oversized image attachments

This commit is contained in:
Ryan Ward
2026-09-24 21:54:20 -05:00
parent 6bf81519dc
commit 2a89d03866
15 changed files with 1606 additions and 283 deletions
+107 -29
View File
@@ -163,11 +163,21 @@ impl From<ProviderRunProtocolError> for ProviderRunCoordinatorError {
pub(crate) struct ProviderRunProfile {
pub(crate) runtime: Arc<dyn AgentRuntime>,
pub(crate) request: TurnRequest,
pub(crate) max_context_tokens: Option<u32>,
}
impl ProviderRunProfile {
pub(crate) fn new(runtime: Arc<dyn AgentRuntime>, request: TurnRequest) -> Self {
Self { runtime, request }
Self {
runtime,
request,
max_context_tokens: None,
}
}
pub(crate) fn with_max_context_tokens(mut self, max_context_tokens: Option<u32>) -> Self {
self.max_context_tokens = max_context_tokens;
self
}
}
@@ -183,23 +193,19 @@ pub(crate) struct ProviderRunCoordinator {
impl ProviderRunCoordinator {
pub(crate) fn from_request(
run_id: impl Into<galaxy_agent_core::ProviderRunId>,
runtime: Arc<dyn AgentRuntime>,
request: TurnRequest,
base_profile: ProviderRunProfile,
tool_result_archive: Vec<galaxy_agent_core::ConversationMessage>,
limits: ProviderRunLimits,
) -> Result<Self, ProviderRunCoordinatorError> {
let mut run = ProviderRun::new(
run_id,
request.messages.clone(),
base_profile.request.messages.clone(),
BASE_PROVIDER_PROFILE,
limits,
);
run.replace_tool_result_archive(tool_result_archive);
let mut profiles = BTreeMap::new();
profiles.insert(
BASE_PROVIDER_PROFILE.to_string(),
ProviderRunProfile::new(runtime, request),
);
profiles.insert(BASE_PROVIDER_PROFILE.to_string(), base_profile);
Self::new(run, profiles)
}
@@ -266,13 +272,11 @@ impl ProviderRunCoordinator {
pub(crate) fn insert_profile(
&mut self,
profile: impl Into<String>,
runtime: Arc<dyn AgentRuntime>,
request: TurnRequest,
config: ProviderRunProfile,
) -> Result<(), ProviderRunCoordinatorError> {
let profile = profile.into();
validate_profile_runtime(&profile, runtime.as_ref())?;
self.profiles
.insert(profile, ProviderRunProfile::new(runtime, request));
validate_profile_runtime(&profile, config.runtime.as_ref())?;
self.profiles.insert(profile, config);
Ok(())
}
@@ -461,13 +465,15 @@ impl ProviderRunCoordinator {
}
fn request_needs_context_compaction(&self, call: &ProviderModelCall) -> bool {
let Some(max_context_tokens) = self.max_context_tokens else {
return false;
};
let Some(profile) = self.profiles.get(call.profile.as_str()) else {
return false;
};
let request = request_for_model_call(profile.request.clone(), call);
let Some(max_context_tokens) = profile.max_context_tokens.or(self.max_context_tokens)
else {
return false;
};
let request =
request_for_model_call(profile.request.clone(), call, Some(max_context_tokens));
// Provider tokenizers and request envelopes differ. Compact before the
// estimate reaches the advertised limit, leaving room for that uncertainty.
let safe_context_tokens = u64::from(max_context_tokens) * 9 / 10;
@@ -547,7 +553,11 @@ impl ProviderRunCoordinator {
{
return Ok(());
}
let request = request_for_model_call(profile.request.clone(), &call);
let request = request_for_model_call(
profile.request.clone(),
&call,
profile.max_context_tokens.or(self.max_context_tokens),
);
let started_at = Instant::now();
let (attempt_sender, attempt_control) = turn_control();
let start_future = profile.runtime.start_turn(request, attempt_control).fuse();
@@ -1132,9 +1142,18 @@ fn validate_profile_runtime(
Ok(())
}
fn request_for_model_call(mut template: TurnRequest, call: &ProviderModelCall) -> TurnRequest {
fn request_for_model_call(
mut template: TurnRequest,
call: &ProviderModelCall,
max_context_tokens: Option<u32>,
) -> TurnRequest {
template.prompt = None;
template.messages = call.messages.clone();
for message in &mut template.messages {
message.truncate_tool_results_for_provider_request_with_context(max_context_tokens);
message
.truncate_tagged_terminal_output_for_provider_request_with_context(max_context_tokens);
}
add_exploration_reminder(&mut template);
template
}
@@ -1206,33 +1225,92 @@ fn consecutive_discovery_batches(messages: &[ConversationMessage]) -> usize {
const ESTIMATED_BYTES_PER_TOKEN: u64 = 3;
const ESTIMATED_REQUEST_OVERHEAD_TOKENS: u64 = 512;
const ESTIMATED_IMAGE_TOKENS: u64 = 1_600;
/// Conservative estimate, not an exact tokenizer count. UTF-8 bytes account for
/// non-ASCII content better than character counts; the caller also leaves headroom.
/// Heuristic estimate, not an exact tokenizer count or provider-specific bound.
/// UTF-8 bytes account for non-ASCII content better than character counts; raw
/// image payload bytes are excluded from text accounting and charged separately.
fn estimate_turn_request_tokens(request: &TurnRequest) -> u64 {
let mut bytes = request
let mut text_bytes = request
.system_prompt
.as_deref()
.map_or(0, |text| text.len());
bytes += request.prompt.as_ref().map_or(0, |prompt| {
serde_json::to_string(prompt).unwrap_or_default().len()
});
bytes += request
text_bytes += request
.prompt
.as_ref()
.map_or(0, estimate_message_content_bytes);
text_bytes += request
.messages
.iter()
.map(|message| serde_json::to_string(message).unwrap_or_default().len())
.map(estimate_conversation_message_bytes)
.sum::<usize>();
bytes += request
text_bytes += request
.tools
.iter()
.map(|tool| serde_json::to_string(tool).unwrap_or_default().len())
.sum::<usize>();
let input_tokens = (bytes as u64).div_ceil(ESTIMATED_BYTES_PER_TOKEN);
let image_tokens = request
.prompt
.as_ref()
.map_or(0, count_images_in_content)
.saturating_add(
request
.messages
.iter()
.map(|message| count_images_in_content(&message.content))
.sum::<u64>(),
)
.saturating_mul(ESTIMATED_IMAGE_TOKENS);
let input_tokens = (text_bytes as u64).div_ceil(ESTIMATED_BYTES_PER_TOKEN);
input_tokens
.saturating_add(image_tokens)
.saturating_add(ESTIMATED_REQUEST_OVERHEAD_TOKENS)
.saturating_add(request.max_output_tokens.unwrap_or_default())
}
fn estimate_conversation_message_bytes(message: &ConversationMessage) -> usize {
let mut sanitized_message = message.clone();
remove_image_payloads_from_content(&mut sanitized_message.content);
serde_json::to_string(&sanitized_message)
.unwrap_or_default()
.len()
}
fn estimate_message_content_bytes(content: &MessageContent) -> usize {
let mut sanitized_content = content.clone();
remove_image_payloads_from_content(&mut sanitized_content);
serde_json::to_string(&sanitized_content)
.unwrap_or_default()
.len()
}
fn remove_image_payloads_from_content(content: &mut MessageContent) {
if let MessageContent::MultiPart(parts) = content {
for part in parts {
match part {
ContentPart::Image { data, .. } => data.clear(),
ContentPart::Text(_)
| ContentPart::Reasoning { .. }
| ContentPart::ToolUse { .. }
| ContentPart::ToolResult { .. } => {}
}
}
}
}
fn count_images_in_content(content: &MessageContent) -> u64 {
match content {
MessageContent::Text(_)
| MessageContent::ToolUse { .. }
| MessageContent::ToolResult { .. } => 0,
MessageContent::MultiPart(parts) => parts
.iter()
.filter(|part| matches!(part, ContentPart::Image { .. }))
.count() as u64,
}
}
fn tool_event_call_id(event: &ToolEvent) -> Result<&str, ProviderRunCoordinatorError> {
match event {
ToolEvent::Proposed { call } => Ok(&call.id),
@@ -190,6 +190,34 @@ fn estimates_turn_request_with_system_tools_messages_and_output() {
);
}
#[test]
fn estimates_image_payloads_without_counting_raw_bytes_as_text() {
let image_bytes = vec![255; 3_750_000];
let serialized_text_tokens = serde_json::to_string(&image_bytes)
.unwrap()
.len()
.div_ceil(ESTIMATED_BYTES_PER_TOKEN as usize) as u64;
let request = TurnRequest::new(
"test-model",
vec![ConversationMessage {
role: MessageRole::User,
content: MessageContent::MultiPart(vec![
ContentPart::Text("what is in this screenshot?".to_string()),
ContentPart::Image {
data: image_bytes,
mime_type: "image/jpeg".to_string(),
},
]),
}],
);
let estimate = estimate_turn_request_tokens(&request);
assert!(estimate < 10_000);
assert!(estimate > ESTIMATED_REQUEST_OVERHEAD_TOKENS + ESTIMATED_IMAGE_TOKENS);
assert!(serialized_text_tokens > estimate);
}
#[tokio::test]
async fn proactively_compacts_before_starting_an_oversized_request() {
let runtime = Arc::new(ScriptedRuntime::new(vec![answer_turn()]));
@@ -276,8 +304,7 @@ fn answer_turn() -> ScriptedTurn {
fn coordinator(runtime: Arc<dyn AgentRuntime>) -> ProviderRunCoordinator {
ProviderRunCoordinator::from_request(
"run-1",
runtime,
request(),
ProviderRunProfile::new(runtime, request()),
Vec::new(),
ProviderRunLimits::default(),
)
@@ -1357,8 +1384,7 @@ fn session_runtime_is_rejected_before_any_turn_can_start() {
});
let error = ProviderRunCoordinator::from_request(
"run-1",
runtime,
request(),
ProviderRunProfile::new(runtime, request()),
Vec::new(),
ProviderRunLimits::default(),
)
@@ -1379,8 +1405,7 @@ fn context_estimate_tracks_live_transcript_and_reserves_fixed_context() {
request.max_output_tokens = Some(4_000);
let mut coordinator = ProviderRunCoordinator::from_request(
"context-budget",
runtime,
request,
ProviderRunProfile::new(runtime, request),
Vec::new(),
ProviderRunLimits::default(),
)
@@ -1402,6 +1427,140 @@ fn context_estimate_tracks_live_transcript_and_reserves_fixed_context() {
assert_eq!(fixed_before, fixed_after);
}
#[tokio::test]
async fn monitor_request_uses_its_profile_context_limit() {
let base_runtime = Arc::new(ScriptedRuntime::new(vec![answer_turn()]));
let cli_runtime = Arc::new(ScriptedRuntime::new(vec![answer_turn()]));
let original_content = format!("start:{}:end", "x".repeat(20_000));
let large_result = ConversationMessage {
role: MessageRole::User,
content: MessageContent::ToolResult {
tool_use_id: "large-result".to_string(),
content: original_content.clone(),
is_error: false,
},
};
let run = ProviderRun::new(
"profile-limit-run",
vec![large_result],
CLI_MONITOR_PROVIDER_PROFILE,
ProviderRunLimits::default(),
);
let mut profiles = BTreeMap::new();
profiles.insert(
BASE_PROVIDER_PROFILE.to_string(),
ProviderRunProfile::new(base_runtime, request()).with_max_context_tokens(Some(100_000)),
);
profiles.insert(
CLI_MONITOR_PROVIDER_PROFILE.to_string(),
ProviderRunProfile::new(cli_runtime.clone(), request())
.with_max_context_tokens(Some(8_000)),
);
let mut coordinator = ProviderRunCoordinator::new(run, profiles).unwrap();
coordinator.set_max_context_tokens(Some(100_000));
let (_sender, control) = turn_control();
let block = coordinator
.drive_until_blocked(control, |_| Ok(()))
.await
.unwrap();
assert!(matches!(block, ProviderRunBlock::AwaitingDriver { .. }));
let requests = cli_runtime.requests();
let MessageContent::ToolResult { content, .. } = &requests[0].messages[0].content else {
panic!("expected bounded monitor tool result");
};
assert!(content.chars().count() <= galaxy_agent_core::provider_output_char_limit(Some(8_000)));
assert!(content.contains("tool result truncated"));
let MessageContent::ToolResult { content, .. } = &coordinator.run().transcript()[0].content
else {
panic!("expected original monitor tool result");
};
assert_eq!(content, &original_content);
}
#[tokio::test]
async fn final_model_request_bounds_restored_tagged_terminal_text() {
let runtime = Arc::new(ScriptedRuntime::new(vec![answer_turn()]));
let original_output = format!("first:{}:last", "x".repeat(20_000));
let restored_message = ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(format!(
"[Running command: verbose]\n[Terminal output:\n{original_output}\n]\nContinue"
)),
};
let mut turn_request = request();
turn_request.messages = vec![restored_message];
let mut coordinator = ProviderRunCoordinator::from_request(
"restored-terminal-run",
ProviderRunProfile::new(runtime.clone(), turn_request).with_max_context_tokens(Some(8_000)),
Vec::new(),
ProviderRunLimits::default(),
)
.unwrap();
let (_sender, control) = turn_control();
let block = coordinator
.drive_until_blocked(control, |_| Ok(()))
.await
.unwrap();
assert!(matches!(block, ProviderRunBlock::AwaitingDriver { .. }));
let requests = runtime.requests();
let MessageContent::Text(content) = &requests[0].messages[0].content else {
panic!("expected bounded restored terminal text");
};
assert!(content.contains("terminal output truncated for model context"));
assert!(!content.contains(&original_output));
let MessageContent::Text(content) = &coordinator.run().transcript()[0].content else {
panic!("expected original restored terminal text");
};
assert!(content.contains(&original_output));
}
#[tokio::test]
async fn final_model_request_bounds_new_tool_results_to_the_active_context() {
let runtime = Arc::new(ScriptedRuntime::new(vec![answer_turn()]));
let original_content = format!("start:{}:end", "界".repeat(20_000));
let large_result = ConversationMessage {
role: MessageRole::User,
content: MessageContent::ToolResult {
tool_use_id: "large-result".to_string(),
content: original_content.clone(),
is_error: false,
},
};
let mut turn_request = request();
turn_request.messages = vec![large_result];
let mut coordinator = ProviderRunCoordinator::from_request(
"large-result-run",
ProviderRunProfile::new(runtime.clone(), turn_request),
Vec::new(),
ProviderRunLimits::default(),
)
.unwrap();
coordinator.set_max_context_tokens(Some(8_000));
let (_sender, control) = turn_control();
let block = coordinator
.drive_until_blocked(control, |_| Ok(()))
.await
.unwrap();
assert!(matches!(block, ProviderRunBlock::AwaitingDriver { .. }));
let requests = runtime.requests();
let MessageContent::ToolResult { content, .. } = &requests[0].messages[0].content else {
panic!("expected bounded tool result");
};
assert!(content.chars().count() <= galaxy_agent_core::provider_output_char_limit(Some(8_000)));
assert!(content.contains("tool result truncated"));
let MessageContent::ToolResult { content, .. } = &coordinator.run().transcript()[0].content
else {
panic!("expected original tool result");
};
assert_eq!(content, &original_content);
}
#[tokio::test]
async fn preflight_leaves_headroom_before_the_advertised_context_limit() {
let runtime = Arc::new(ScriptedRuntime::new(vec![answer_turn()]));
+8 -8
View File
@@ -129,11 +129,6 @@ pub(crate) async fn prepare_provider_run(
let (supported_tools, supported_cli_agent_tools) =
crate::ai::agent::api::prepare_direct_provider_params(&mut params);
let skill_path_origin = params.session_context.skill_path_origin();
let max_context_tokens = provider_context_window_tokens(
&base_provider_config,
params.model.as_str(),
params.context_window_limit,
);
let mut cli_params = params.clone();
let cli_model_is_placeholder = params.cli_agent_model.as_str().trim().is_empty()
|| params
@@ -178,9 +173,13 @@ pub(crate) async fn prepare_provider_run(
Some(RigRequestMode::Cli),
)
.await?;
let cli_monitor_profile = Some(ProviderRunProfile::new(cli_runtime, cli_prepared.request));
let cli_monitor_profile = Some(
ProviderRunProfile::new(cli_runtime, cli_prepared.request)
.with_max_context_tokens(cli_prepared.max_context_tokens),
);
let PreparedRigTurn {
max_context_tokens: prepared_context_tokens,
task_id,
needs_create_task,
user_query,
@@ -203,12 +202,13 @@ pub(crate) async fn prepare_provider_run(
needs_create_task,
user_query,
model_id: request.model.as_str().to_string(),
max_context_tokens,
max_context_tokens: prepared_context_tokens,
capabilities: base_runtime.descriptor().capabilities.clone(),
empty_output_message: None,
};
Ok(PreparedProviderRun {
base_profile: ProviderRunProfile::new(base_runtime, request),
base_profile: ProviderRunProfile::new(base_runtime, request)
.with_max_context_tokens(prepared_context_tokens),
cli_monitor_profile,
tool_result_archive,
messages_sent,
+59 -17
View File
@@ -6,9 +6,10 @@ use ai::skills::SkillReference;
use base64::engine::general_purpose;
use base64::Engine as _;
use galaxy_agent_core::{
ContentPart, ConversationMessage, MessageContent, MessageRole, ToolDefinition, ToolResult,
TurnRequest,
bounded_terminal_output_for_provider_request, ContentPart, ConversationMessage, MessageContent,
MessageRole, ToolDefinition, ToolResult, TurnRequest,
};
use galaxy_bedrock_model_catalog::model_metadata;
use sha2::{Digest as _, Sha256};
use uuid::Uuid;
use warp_multi_agent_api as api;
@@ -26,6 +27,7 @@ use crate::ai::provider::request_translator::{
use crate::ai::provider::types::flatten_tool_history_for_no_tools_turn;
pub(crate) struct PreparedRigTurn {
pub max_context_tokens: Option<u32>,
pub task_id: String,
pub needs_create_task: bool,
pub user_query: Option<String>,
@@ -135,9 +137,12 @@ pub(crate) fn prepare_rig_turn(
supported_tools: Vec<ToolType>,
supported_cli_agent_tools: Vec<ToolType>,
) -> PreparedRigTurn {
let max_context_tokens =
effective_context_limit(config.max_input_tokens, params.context_window_limit);
prepare_rig_turn_for_provider(
config.model.clone(),
config.max_output_tokens.map(u64::from),
max_context_tokens,
RigRequestSanitizer::OpenAICompatible,
params,
supported_tools,
@@ -153,9 +158,12 @@ pub(crate) fn prepare_rig_turn_for_mode(
supported_cli_agent_tools: Vec<ToolType>,
mode: RigRequestMode,
) -> PreparedRigTurn {
let max_context_tokens =
effective_context_limit(config.max_input_tokens, params.context_window_limit);
prepare_rig_turn_for_provider(
config.model.clone(),
config.max_output_tokens.map(u64::from),
max_context_tokens,
RigRequestSanitizer::OpenAICompatible,
params,
supported_tools,
@@ -189,9 +197,12 @@ pub(crate) fn prepare_bedrock_rig_turn_for_mode(
supported_cli_agent_tools: Vec<ToolType>,
mode: Option<RigRequestMode>,
) -> PreparedRigTurn {
let provider_limit = model_metadata(&model).and_then(|metadata| metadata.context_window_tokens);
let max_context_tokens = effective_context_limit(provider_limit, params.context_window_limit);
prepare_rig_turn_for_provider(
Some(model),
max_output_tokens,
max_context_tokens,
RigRequestSanitizer::Bedrock,
params,
supported_tools,
@@ -206,9 +217,22 @@ enum RigRequestSanitizer {
Bedrock,
}
fn effective_context_limit(
provider_limit: Option<u32>,
configured_limit: Option<u32>,
) -> Option<u32> {
match (provider_limit, configured_limit) {
(Some(provider), Some(configured)) => Some(provider.min(configured)),
(provider, configured) => provider.or(configured),
}
}
// Keep provider limits and tool sets explicit at this shared request adapter boundary.
#[allow(clippy::too_many_arguments)]
fn prepare_rig_turn_for_provider(
model_override: Option<String>,
max_output_tokens: Option<u64>,
max_context_tokens: Option<u32>,
sanitizer: RigRequestSanitizer,
params: RequestParams,
supported_tools: Vec<ToolType>,
@@ -269,11 +293,13 @@ fn prepare_rig_turn_for_provider(
}
let mut system_prompt = build_system_prompt(&input, &tools, &global_rules, mode);
let mut new_messages = input_messages(input, tool_results);
let mut new_messages = input_messages(input, tool_results, max_context_tokens);
let mut persistent_messages = message_history;
persistent_messages.append(&mut new_messages);
for message in &mut persistent_messages {
message.truncate_tool_results_for_provider_request();
message.truncate_tool_results_for_provider_request_with_context(max_context_tokens);
message
.truncate_tagged_terminal_output_for_provider_request_with_context(max_context_tokens);
}
match sanitizer {
RigRequestSanitizer::OpenAICompatible => {
@@ -311,6 +337,7 @@ fn prepare_rig_turn_for_provider(
request.max_output_tokens = max_output_tokens;
PreparedRigTurn {
max_context_tokens,
task_id,
needs_create_task,
user_query,
@@ -338,6 +365,7 @@ fn model_workflow_guidance(model_id: &str) -> &'static str {
fn input_messages(
inputs: Vec<AIAgentInput>,
tool_results: Vec<ToolResult>,
max_context_tokens: Option<u32>,
) -> Vec<ConversationMessage> {
let mut messages = Vec::new();
if !tool_results.is_empty() {
@@ -375,11 +403,18 @@ fn input_messages(
});
}
messages.extend(inputs.into_iter().filter_map(input_message));
messages.extend(
inputs
.into_iter()
.filter_map(|input| input_message(input, max_context_tokens)),
);
messages
}
fn input_message(input: AIAgentInput) -> Option<ConversationMessage> {
fn input_message(
input: AIAgentInput,
max_context_tokens: Option<u32>,
) -> Option<ConversationMessage> {
let (text, images) = match input {
AIAgentInput::UserQuery {
query,
@@ -388,9 +423,13 @@ fn input_message(input: AIAgentInput) -> Option<ConversationMessage> {
..
} => {
let text = if let Some(command) = running_command {
let output = bounded_terminal_output_for_provider_request(
&command.grid_contents,
max_context_tokens,
);
format!(
"[Running command: {}]\n[Command ID: {}]\n[Terminal output:\n{}\n]\n{}",
command.command, command.block_id, command.grid_contents, query
command.command, command.block_id, output, query
)
} else {
query
@@ -401,16 +440,19 @@ fn input_message(input: AIAgentInput) -> Option<ConversationMessage> {
prompt,
context,
completed_command,
} => (
format!(
"[Completed command: {}]\n[Command ID: {}]\n[Final terminal output:\n{}\n]\n{}",
completed_command.command,
completed_command.block_id,
completed_command.grid_contents,
prompt
),
image_parts(&context),
),
} => {
let output = bounded_terminal_output_for_provider_request(
&completed_command.grid_contents,
max_context_tokens,
);
(
format!(
"[Completed command: {}]\n[Command ID: {}]\n[Final terminal output:\n{}\n]\n{}",
completed_command.command, completed_command.block_id, output, prompt
),
image_parts(&context),
)
}
AIAgentInput::ActionResult { .. } => return None,
AIAgentInput::AutoCodeDiffQuery { query, .. } => (query, Vec::new()),
AIAgentInput::ResumeConversation { .. } => (
+33 -1
View File
@@ -437,6 +437,38 @@ fn completed_command_assessment_uses_root_history_without_tools_or_monitor_instr
assert_eq!(prepared.request.messages, prepared.persistent_messages);
}
#[test]
fn completed_command_assessment_bounds_only_the_provider_payload() {
let full_output = format!("first\n{}\nlast", "x".repeat(100_000));
let block_id: galaxy_terminal::model::BlockId = "large-completed-command".to_string().into();
let mut params = RequestParams::new_for_test();
params.context_window_limit = Some(8_000);
params.input = vec![AIAgentInput::CommandCompletionAssessment {
prompt: "Report the final result to the user.".to_string(),
context: Arc::from([]),
completed_command: crate::ai::agent::RunningCommand {
command: "verbose-command".to_string(),
block_id,
grid_contents: full_output.clone(),
cursor: String::new(),
requested_command_id: None,
is_alt_screen_active: false,
},
}];
let prepared = prepare_rig_turn(&config(), params, Vec::new(), Vec::new());
let MessageContent::Text(provider_text) = &prepared.request.messages[0].content else {
panic!("expected text command assessment");
};
assert!(provider_text.contains("terminal output truncated for model context"));
assert!(provider_text.contains("full output remains available in the terminal"));
assert!(provider_text.contains("first"));
assert!(provider_text.contains("last"));
assert!(!provider_text.contains(&full_output));
assert_eq!(full_output.chars().count(), 100_011);
}
#[test]
fn forced_cli_profile_uses_monitor_prompt_and_tools_for_an_initial_query() {
let mut params = RequestParams::new_for_test();
@@ -801,7 +833,7 @@ fn normalized_tool_outcomes_are_the_only_action_results_sent_to_rig() {
})
.collect();
let messages = input_messages(Vec::new(), tool_results);
let messages = input_messages(Vec::new(), tool_results, Some(128_000));
assert_eq!(messages.len(), 1);
let MessageContent::MultiPart(parts) = &messages[0].content else {