diff --git a/src/apps/desktop/src/computer_use/windows_ax_ui.rs b/src/apps/desktop/src/computer_use/windows_ax_ui.rs
index c102ccd584..bbcb411f84 100644
--- a/src/apps/desktop/src/computer_use/windows_ax_ui.rs
+++ b/src/apps/desktop/src/computer_use/windows_ax_ui.rs
@@ -305,9 +305,9 @@ fn read_cached_is_offscreen(element: &IUIAutomationElement) -> bool {
/// Read bounding rect as `(center_x, center_y, Some((l, t, r, b)))`. Returns
/// `rect=None` when the element has no meaningful `BoundingRectangle`.
-fn read_cached_bounding_rect_full(
- element: &IUIAutomationElement,
-) -> (i32, i32, Option<(i32, i32, i32, i32)>) {
+type CachedBoundingRect = (i32, i32, Option<(i32, i32, i32, i32)>);
+
+fn read_cached_bounding_rect_full(element: &IUIAutomationElement) -> CachedBoundingRect {
unsafe {
match element.CachedBoundingRectangle() {
Ok(r) if r.right > r.left && r.bottom > r.top => (
diff --git a/src/apps/desktop/src/computer_use/windows_bg_input.rs b/src/apps/desktop/src/computer_use/windows_bg_input.rs
index 11a6322680..9c07c860c5 100644
--- a/src/apps/desktop/src/computer_use/windows_bg_input.rs
+++ b/src/apps/desktop/src/computer_use/windows_bg_input.rs
@@ -132,7 +132,7 @@ fn il_name(rid: u32) -> &'static str {
#[repr(C)]
#[derive(Clone, Copy)]
-struct KEYBDINPUT {
+struct KeybdInput {
wVk: u16,
wScan: u16,
dwFlags: u32,
@@ -142,7 +142,7 @@ struct KEYBDINPUT {
#[repr(C)]
#[derive(Clone, Copy)]
-struct MOUSEINPUT {
+struct MouseInput {
dx: i32,
dy: i32,
mouseData: u32,
@@ -153,7 +153,7 @@ struct MOUSEINPUT {
#[repr(C)]
#[derive(Clone, Copy)]
-struct HARDWAREINPUT {
+struct HardwareInput {
uMsg: u32,
wParamL: u16,
wParamH: u16,
@@ -164,14 +164,14 @@ struct HARDWAREINPUT {
#[repr(C)]
#[derive(Clone, Copy)]
union INPUT_0 {
- ki: KEYBDINPUT,
- mi: MOUSEINPUT,
- hi: HARDWAREINPUT,
+ ki: KeybdInput,
+ mi: MouseInput,
+ hi: HardwareInput,
}
#[repr(C)]
#[derive(Clone, Copy)]
-struct INPUT {
+struct Input {
r#type: u32,
Anonymous: INPUT_0,
}
@@ -190,7 +190,7 @@ struct TOKEN_MANDATORY_LABEL {
#[link(name = "user32")]
extern "system" {
- fn SendInput(c_inputs: u32, p_inputs: *const INPUT, cb_size: i32) -> u32;
+ fn SendInput(c_inputs: u32, p_inputs: *const Input, cb_size: i32) -> u32;
fn AttachThreadInput(id_attach: u32, id_attach_to: u32, f_attach: i32) -> i32;
fn MapVirtualKeyW(code: u32, map_type: u32) -> u32;
/// `VkKeyScanW` — translate a Unicode char to a virtual-key code + shift
@@ -846,15 +846,15 @@ fn make_key_lparam(scan: u32, down: bool) -> LPARAM {
/// One `SendInput` keyboard event carrying a Unicode code unit (`KEYEVENTF_
/// UNICODE`). `up` adds `KEYEVENTF_KEYUP`.
-fn unicode_event(unit: u16, up: bool) -> INPUT {
+fn unicode_event(unit: u16, up: bool) -> Input {
let mut flags = KEYEVENTF_UNICODE;
if up {
flags |= KEYEVENTF_KEYUP;
}
- INPUT {
+ Input {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
- ki: KEYBDINPUT {
+ ki: KeybdInput {
wVk: 0,
wScan: unit,
dwFlags: flags,
@@ -867,12 +867,12 @@ fn unicode_event(unit: u16, up: bool) -> INPUT {
/// One `SendInput` keyboard event for a virtual-key code. `up` adds
/// `KEYEVENTF_KEYUP`.
-fn vk_event(vk: u16, scan: u32, up: bool) -> INPUT {
+fn vk_event(vk: u16, scan: u32, up: bool) -> Input {
let flags = if up { KEYEVENTF_KEYUP } else { 0 };
- INPUT {
+ Input {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
- ki: KEYBDINPUT {
+ ki: KeybdInput {
wVk: vk,
wScan: scan as u16,
dwFlags: flags,
@@ -889,7 +889,7 @@ fn vk_event(vk: u16, scan: u32, up: bool) -> INPUT {
/// `SendInput` reads `ev.len()` `INPUT` records from `ev.as_ptr()`; every
/// record is fully initialized above. `cbSize` is the true `size_of::`.
unsafe fn send_unicode(text: &str) -> BitFunResult<()> {
- let mut ev: Vec = Vec::with_capacity(text.len() * 2);
+ let mut ev: Vec = Vec::with_capacity(text.len() * 2);
for u in text.encode_utf16() {
ev.push(unicode_event(u, false));
ev.push(unicode_event(u, true));
@@ -900,7 +900,7 @@ unsafe fn send_unicode(text: &str) -> BitFunResult<()> {
let sent = SendInput(
ev.len() as u32,
ev.as_ptr(),
- std::mem::size_of::() as i32,
+ std::mem::size_of::() as i32,
);
if sent as usize != ev.len() {
return Err(BitFunError::service(format!(
@@ -917,7 +917,7 @@ unsafe fn send_unicode(text: &str) -> BitFunResult<()> {
/// # Safety
/// `SendInput` reads a fully-initialized `INPUT` array; `cbSize` is correct.
unsafe fn send_key_combo(keycode: u16, modifiers: &[u16]) -> BitFunResult<()> {
- let mut ev: Vec = Vec::with_capacity(modifiers.len() * 2 + 2);
+ let mut ev: Vec = Vec::with_capacity(modifiers.len() * 2 + 2);
for &m in modifiers {
let m_scan = MapVirtualKeyW(m as u32, MAPVK_VK_TO_VSC);
ev.push(vk_event(m, m_scan, false));
@@ -935,7 +935,7 @@ unsafe fn send_key_combo(keycode: u16, modifiers: &[u16]) -> BitFunResult<()> {
let sent = SendInput(
ev.len() as u32,
ev.as_ptr(),
- std::mem::size_of::() as i32,
+ std::mem::size_of::() as i32,
);
if sent as usize != ev.len() {
return Err(BitFunError::service(format!(
@@ -1268,3 +1268,55 @@ pub(super) fn parse_key_chord(keys: &[String]) -> BitFunResult<(Vec, u16)>
};
Ok((modifiers, keycode))
}
+
+#[cfg(test)]
+mod tests {
+ use super::{HardwareInput, Input, KeybdInput, MouseInput, INPUT_0};
+ use std::mem::{align_of, offset_of, size_of};
+
+ #[test]
+ fn send_input_ffi_layout_matches_winuser() {
+ assert_eq!(size_of::(), 8);
+ assert_eq!(align_of::(), 4);
+ assert_eq!(offset_of!(HardwareInput, uMsg), 0);
+ assert_eq!(offset_of!(HardwareInput, wParamL), 4);
+ assert_eq!(offset_of!(HardwareInput, wParamH), 6);
+
+ if cfg!(target_pointer_width = "64") {
+ assert_eq!(size_of::(), 24);
+ assert_eq!(align_of::(), 8);
+ assert_eq!(offset_of!(KeybdInput, dwExtraInfo), 16);
+ assert_eq!(size_of::(), 32);
+ assert_eq!(align_of::(), 8);
+ assert_eq!(offset_of!(MouseInput, dwExtraInfo), 24);
+ assert_eq!(size_of::(), 32);
+ assert_eq!(align_of::(), 8);
+ assert_eq!(size_of::(), 40);
+ assert_eq!(align_of::(), 8);
+ assert_eq!(offset_of!(Input, Anonymous), 8);
+ } else {
+ assert_eq!(size_of::(), 16);
+ assert_eq!(align_of::(), 4);
+ assert_eq!(offset_of!(KeybdInput, dwExtraInfo), 12);
+ assert_eq!(size_of::(), 24);
+ assert_eq!(align_of::(), 4);
+ assert_eq!(offset_of!(MouseInput, dwExtraInfo), 20);
+ assert_eq!(size_of::(), 24);
+ assert_eq!(align_of::(), 4);
+ assert_eq!(size_of::(), 28);
+ assert_eq!(align_of::(), 4);
+ assert_eq!(offset_of!(Input, Anonymous), 4);
+ }
+
+ assert_eq!(offset_of!(KeybdInput, wVk), 0);
+ assert_eq!(offset_of!(KeybdInput, wScan), 2);
+ assert_eq!(offset_of!(KeybdInput, dwFlags), 4);
+ assert_eq!(offset_of!(KeybdInput, time), 8);
+ assert_eq!(offset_of!(MouseInput, dx), 0);
+ assert_eq!(offset_of!(MouseInput, dy), 4);
+ assert_eq!(offset_of!(MouseInput, mouseData), 8);
+ assert_eq!(offset_of!(MouseInput, dwFlags), 12);
+ assert_eq!(offset_of!(MouseInput, time), 16);
+ assert_eq!(offset_of!(Input, r#type), 0);
+ }
+}
diff --git a/src/crates/assembly/core/src/agentic/execution/execution_engine.rs b/src/crates/assembly/core/src/agentic/execution/execution_engine.rs
index 1a182510a1..ff5fce6ae7 100644
--- a/src/crates/assembly/core/src/agentic/execution/execution_engine.rs
+++ b/src/crates/assembly/core/src/agentic/execution/execution_engine.rs
@@ -289,6 +289,45 @@ struct CompressionTriggerBudget {
safety_reserve_tokens: usize,
}
+// Fields are declared in reverse parameter order so dropping an unconsumed
+// input preserves the previous function-parameter drop order. Call sites keep
+// struct literal fields in the original evaluation order.
+struct TurnPromptScaffoldInput<'a> {
+ stage: &'a str,
+ runtime_context_needs: RuntimeContextNeeds,
+ tool_listing_sections: ToolListingSections,
+ supports_image_understanding: bool,
+ model_name: &'a str,
+ current_agent: &'a dyn crate::agentic::agents::Agent,
+ context: &'a ExecutionContext,
+}
+
+struct FinalizeRoundInput<'a> {
+ context_window: usize,
+ tool_definitions: Option>,
+ reminder_text: &'a str,
+ messages: &'a [Message],
+ prepended_reminders: &'a [&'a str],
+ primary_model_facts: &'a PrimaryModelFacts,
+ execution_context_vars: &'a HashMap,
+ round_group_id: Option,
+ round_number: usize,
+ agent_type: String,
+ context: &'a ExecutionContext,
+ ai_client: Arc,
+}
+
+struct CompressionModelSummaryInput<'a> {
+ trace_config: Option,
+ primary_supports_image_understanding: bool,
+ prepended_prompt_reminders: &'a PrependedPromptReminders,
+ tool_definitions: &'a Option>,
+ workspace: Option<&'a WorkspaceBinding>,
+ dialog_turn_id: &'a str,
+ runtime_messages: &'a [Message],
+ ai_client: Arc,
+}
+
/// Execution engine
pub struct ExecutionEngine {
round_executor: Arc,
@@ -1079,51 +1118,45 @@ impl ExecutionEngine {
async fn resolve_turn_prompt_scaffold(
&self,
- context: &ExecutionContext,
- current_agent: &dyn crate::agentic::agents::Agent,
- model_name: &str,
- supports_image_understanding: bool,
- tool_listing_sections: ToolListingSections,
- runtime_context_needs: RuntimeContextNeeds,
- stage: &str,
+ input: TurnPromptScaffoldInput<'_>,
) -> BitFunResult {
debug!(
"Resolving turn prompt scaffold: session_id={}, turn_id={}, stage={}, agent={}, model={}",
- context.session_id,
- context.dialog_turn_id,
- stage,
- current_agent.name(),
- model_name
+ input.context.session_id,
+ input.context.dialog_turn_id,
+ input.stage,
+ input.current_agent.name(),
+ input.model_name
);
let prompt_context = Self::build_prompt_context(
- context,
- model_name,
- supports_image_understanding,
- tool_listing_sections,
- runtime_context_needs,
+ input.context,
+ input.model_name,
+ input.supports_image_understanding,
+ input.tool_listing_sections,
+ input.runtime_context_needs,
)
.await;
let prepended_prompt_reminders = self
.build_cached_prepended_prompt_reminders(
- &context.session_id,
- current_agent,
+ &input.context.session_id,
+ input.current_agent,
prompt_context.as_ref(),
- &context.context,
+ &input.context.context,
)
.await;
let system_prompt = self
.resolve_cached_system_prompt(
- &context.session_id,
- current_agent,
+ &input.context.session_id,
+ input.current_agent,
prompt_context.as_ref(),
)
.await?;
Self::log_turn_prompt_scaffold(
- &context.session_id,
- &context.dialog_turn_id,
- stage,
+ &input.context.session_id,
+ &input.context.dialog_turn_id,
+ input.stage,
system_prompt.len(),
&prepended_prompt_reminders,
);
@@ -1297,81 +1330,68 @@ impl ExecutionEngine {
.collect()
}
- async fn run_finalize_round(
- &self,
- ai_client: Arc,
- context: &ExecutionContext,
- agent_type: String,
- round_number: usize,
- round_group_id: Option,
- execution_context_vars: &HashMap,
- primary_model_facts: &PrimaryModelFacts,
- prepended_reminders: &[&str],
- messages: &[Message],
- reminder_text: &str,
- tool_definitions: Option>,
- context_window: usize,
- ) -> BitFunResult {
+ async fn run_finalize_round(&self, input: FinalizeRoundInput<'_>) -> BitFunResult {
// Keep the original tool definitions attached to the finalize request
// even though finalize forbids tool execution at runtime. Dropping the
// tools here would change the provider request shape, which breaks
// prompt/prefix cache reuse and turns the finalize round into a cache
// miss for providers that key caching on the full request schema.
- let finalize_tool_names = Self::finalize_tool_names(tool_definitions.as_deref());
+ let finalize_tool_names = Self::finalize_tool_names(input.tool_definitions.as_deref());
let finalize_runtime_tool_restrictions =
- Self::finalize_runtime_tool_restrictions(context, &finalize_tool_names);
+ Self::finalize_runtime_tool_restrictions(input.context, &finalize_tool_names);
let mut final_ai_messages = Self::build_ai_messages_for_send(
- messages,
- &ai_client.config.format,
- context
+ input.messages,
+ &input.ai_client.config.format,
+ input
+ .context
.workspace
.as_ref()
.map(|workspace| workspace.root_path()),
- &context.dialog_turn_id,
- primary_model_facts.supports_image_inputs,
- prepended_reminders,
+ &input.context.dialog_turn_id,
+ input.primary_model_facts.supports_image_inputs,
+ input.prepended_reminders,
)
.await?;
- final_ai_messages.push(AIMessage::user(render_system_reminder(reminder_text)));
+ final_ai_messages.push(AIMessage::user(render_system_reminder(input.reminder_text)));
final_ai_messages.push(AIMessage::user(Self::FINALIZE_USER_FOLLOWUP.to_string()));
let model_exchange_trace_dir = self
.session_manager
- .persistent_model_exchange_trace_dir(&context.session_id)
+ .persistent_model_exchange_trace_dir(&input.context.session_id)
.await;
let round_context = RoundContext {
- session_id: context.session_id.clone(),
- subagent_parent_info: context.subagent_parent_info.clone(),
- dialog_turn_id: context.dialog_turn_id.clone(),
- turn_index: context.turn_index,
- round_number,
- round_group_id,
- workspace: context.workspace.clone(),
+ session_id: input.context.session_id.clone(),
+ subagent_parent_info: input.context.subagent_parent_info.clone(),
+ dialog_turn_id: input.context.dialog_turn_id.clone(),
+ turn_index: input.context.turn_index,
+ round_number: input.round_number,
+ round_group_id: input.round_group_id,
+ workspace: input.context.workspace.clone(),
model_exchange_trace_dir,
available_tools: finalize_tool_names,
collapsed_tools: Vec::new(),
unlocked_collapsed_tools: Vec::new(),
- model_name: ai_client.config.model.clone(),
- primary_model_facts: primary_model_facts.clone(),
- agent_type,
- context_vars: execution_context_vars.clone(),
- delegation_policy: context.delegation_policy,
+ model_name: input.ai_client.config.model.clone(),
+ primary_model_facts: input.primary_model_facts.clone(),
+ agent_type: input.agent_type,
+ context_vars: input.execution_context_vars.clone(),
+ delegation_policy: input.context.delegation_policy,
runtime_tool_restrictions: finalize_runtime_tool_restrictions,
steering_interrupt: None,
cancellation_token: CancellationToken::new(),
- workspace_services: context.workspace_services.clone(),
- terminal_port: context.terminal_port.clone(),
- remote_exec_port: context.remote_exec_port.clone(),
- recover_partial_on_cancel: context.recover_partial_on_cancel,
+ workspace_services: input.context.workspace_services.clone(),
+ terminal_port: input.context.terminal_port.clone(),
+ remote_exec_port: input.context.remote_exec_port.clone(),
+ recover_partial_on_cancel: input.context.recover_partial_on_cancel,
};
self.round_executor
.execute_round(
- ai_client,
+ input.ai_client,
round_context,
final_ai_messages,
- tool_definitions,
- Some(context_window),
+ input.tool_definitions,
+ Some(input.context_window),
)
.await
}
@@ -1679,32 +1699,25 @@ impl ExecutionEngine {
async fn generate_compression_model_summary(
&self,
- ai_client: Arc,
- runtime_messages: &[Message],
- dialog_turn_id: &str,
- workspace: Option<&WorkspaceBinding>,
- tool_definitions: &Option>,
- prepended_prompt_reminders: &PrependedPromptReminders,
- primary_supports_image_understanding: bool,
- trace_config: Option,
+ input: CompressionModelSummaryInput<'_>,
) -> BitFunResult