Skip to content
Open
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
8 changes: 6 additions & 2 deletions scripts/core-boundaries/rules/source/required-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2994,8 +2994,12 @@ export const requiredContentRules = [
message: 'missing delegation policy parsing owner',
},
{
regex: /\bpub fn primary_model_supports_image_understanding\b/,
message: 'missing model image-support policy owner',
regex: /\bpub struct PrimaryModelFacts\b/,
message: 'missing typed primary model facts owner',
},
{
regex: /\bpub fn multimodal_tool_output_supported\b/,
message: 'missing primary model multimodal tool-output policy owner',
},
{
regex: /\bmaterializes_provider_neutral_tool_custom_data\b/,
Expand Down
93 changes: 44 additions & 49 deletions src/crates/assembly/core/src/agentic/execution/execution_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use tool_runtime::context::PrimaryModelFacts;

/// Execution engine configuration
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -658,9 +659,9 @@ impl ExecutionEngine {
async fn resolve_primary_model_context(
model_id: &str,
ai_client_model: &str,
ai_client_provider: &str,
ai_client_api_format: &str,
unavailable_log_message: &str,
) -> (String, bool) {
) -> PrimaryModelFacts {
let config_service = get_global_config_service().await.ok();
if let Some(service) = config_service {
let ai_config: crate::service::config::types::AIConfig =
Expand All @@ -680,7 +681,7 @@ impl ExecutionEngine {
})
.or_else(|| {
ai_config.models.iter().find(|m| {
m.model_name == ai_client_model && m.provider == ai_client_provider
m.model_name == ai_client_model && m.provider == ai_client_api_format
})
});

Expand All @@ -691,10 +692,10 @@ impl ExecutionEngine {
|| matches!(m.category, ModelCategory::Multimodal)
});

(resolved_id, supports)
PrimaryModelFacts::new(resolved_id, ai_client_model, ai_client_api_format, supports)
} else {
warn!("{}", unavailable_log_message);
(model_id.to_string(), false)
PrimaryModelFacts::new(model_id, ai_client_model, ai_client_api_format, false)
}
}

Expand Down Expand Up @@ -1116,7 +1117,7 @@ impl ExecutionEngine {
round_number: usize,
round_group_id: Option<String>,
execution_context_vars: &HashMap<String, String>,
primary_supports_image_understanding: bool,
primary_model_facts: &PrimaryModelFacts,
prepended_reminders: &[&str],
messages: &[Message],
reminder_text: &str,
Expand All @@ -1139,7 +1140,7 @@ impl ExecutionEngine {
.as_ref()
.map(|workspace| workspace.root_path()),
&context.dialog_turn_id,
primary_supports_image_understanding,
primary_model_facts.supports_image_inputs,
prepended_reminders,
)
.await?;
Expand All @@ -1159,6 +1160,7 @@ impl ExecutionEngine {
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,
Expand Down Expand Up @@ -1570,14 +1572,15 @@ impl ExecutionEngine {
))
})?;

let (resolved_primary_model_id, primary_supports_image_understanding) =
Self::resolve_primary_model_context(
&model_id,
&ai_client.config.model,
&ai_client.config.format,
"Config service unavailable, assuming compression model is text-only for image input gating",
)
.await;
let primary_model_facts = Self::resolve_primary_model_context(
&model_id,
&ai_client.config.model,
&ai_client.config.format,
"Config service unavailable, assuming compression model is text-only for image input gating",
)
.await;
let resolved_primary_model_id = primary_model_facts.model_id.clone();
let primary_supports_image_understanding = primary_model_facts.supports_image_inputs;

let model_capability_profile = ModelCapabilityProfile::from_resolved_model(
&resolved_primary_model_id,
Expand Down Expand Up @@ -1613,10 +1616,7 @@ impl ExecutionEngine {
&context.agent_type,
context.workspace.as_ref(),
context.workspace_services.as_ref(),
Some(&resolved_primary_model_id),
Some(&ai_client.config.model),
Some(&ai_client.config.format),
primary_supports_image_understanding,
Some(&primary_model_facts),
&tool_manifest_context_vars,
);
let tool_manifest = if enable_tools {
Expand Down Expand Up @@ -2212,14 +2212,15 @@ impl ExecutionEngine {
})?;

// Primary model vision capability (tools + system prompt appendix; also used below for API message stripping).
let (resolved_primary_model_id, primary_supports_image_understanding) =
Self::resolve_primary_model_context(
&model_id,
&ai_client.config.model,
&ai_client.config.format,
"Config service unavailable, assuming primary model is text-only for image input gating",
)
.await;
let primary_model_facts = Self::resolve_primary_model_context(
&model_id,
&ai_client.config.model,
&ai_client.config.format,
"Config service unavailable, assuming primary model is text-only for image input gating",
)
.await;
let resolved_primary_model_id = primary_model_facts.model_id.clone();
let primary_supports_image_understanding = primary_model_facts.supports_image_inputs;

let model_context_window = ai_client.config.context_window as usize;
let session_max_tokens = session.config.max_context_tokens;
Expand Down Expand Up @@ -2277,10 +2278,7 @@ impl ExecutionEngine {
&agent_type,
context.workspace.as_ref(),
context.workspace_services.as_ref(),
Some(&resolved_primary_model_id),
Some(&ai_client.config.model),
Some(&ai_client.config.format),
primary_supports_image_understanding,
Some(&primary_model_facts),
&tool_manifest_context_vars,
);

Expand Down Expand Up @@ -2334,6 +2332,18 @@ impl ExecutionEngine {
} else {
(vec![], None)
};
let final_tool_names = Self::finalize_tool_names(tool_definitions.as_deref());
debug!(
"Primary model and tool manifest resolved: session_id={}, turn_id={}, resolved_primary_model_id={}, primary_model_api_format={}, primary_model_supports_image_inputs={}, final_tool_count={}, final_tool_names={:?}, collapsed_tool_names={:?}",
context.session_id,
context.dialog_turn_id,
primary_model_facts.model_id,
primary_model_facts.api_format,
primary_model_facts.supports_image_inputs,
final_tool_names.len(),
final_tool_names,
collapsed_tools,
);

// 4. Resolve the prompt scaffold used by model requests in this turn.
// It is refreshed after successful context compression so the first
Expand Down Expand Up @@ -2403,22 +2413,6 @@ impl ExecutionEngine {
let compression_threshold = session.config.compression_threshold;

let mut execution_context_vars = context.context.clone();
execution_context_vars.insert(
"primary_model_id".to_string(),
resolved_primary_model_id.clone(),
);
execution_context_vars.insert(
"primary_model_name".to_string(),
ai_client.config.model.clone(),
);
execution_context_vars.insert(
"primary_model_provider".to_string(),
ai_client.config.format.clone(),
);
execution_context_vars.insert(
"primary_model_supports_image_understanding".to_string(),
primary_supports_image_understanding.to_string(),
);
execution_context_vars.insert("turn_index".to_string(), context.turn_index.to_string());

// If the primary model is text-only, do not send image payloads to the provider.
Expand Down Expand Up @@ -2632,6 +2626,7 @@ impl ExecutionEngine {
collapsed_tools: collapsed_tools.clone(),
unlocked_collapsed_tools,
model_name: ai_client.config.model.clone(),
primary_model_facts: primary_model_facts.clone(),
agent_type: agent_type.clone(),
context_vars: round_context_vars,
delegation_policy: context.delegation_policy,
Expand Down Expand Up @@ -3120,7 +3115,7 @@ impl ExecutionEngine {
completed_rounds,
finalize_round_group_id.clone(),
&execution_context_vars,
primary_supports_image_understanding,
&primary_model_facts,
&finalize_prepended_reminders,
&messages,
finalize_reminder,
Expand Down Expand Up @@ -3150,7 +3145,7 @@ impl ExecutionEngine {
completed_rounds,
finalize_round_group_id.clone(),
&execution_context_vars,
primary_supports_image_understanding,
&primary_model_facts,
&finalize_prepended_reminders,
&messages,
finalize_reminder,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@ impl RoundExecutor {
attempt_index: Some((attempt_index + 1) as u32),
agent_type: context.agent_type.clone(),
workspace: context.workspace.clone(),
primary_model_facts: context.primary_model_facts.clone(),
context_vars: context.context_vars.clone(),
subagent_parent_info,
delegation_policy: context.delegation_policy,
Expand Down Expand Up @@ -1345,6 +1346,9 @@ mod tests {
collapsed_tools: Vec::new(),
unlocked_collapsed_tools: Vec::new(),
model_name: "model-1".to_string(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::new(
"model-1", "model-1", "openai", true,
),
agent_type: "agentic".to_string(),
context_vars: HashMap::new(),
delegation_policy: DelegationPolicy::top_level(),
Expand Down
2 changes: 2 additions & 0 deletions src/crates/assembly/core/src/agentic/execution/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use tool_runtime::context::PrimaryModelFacts;

/// Execution context
#[derive(Clone)]
Expand Down Expand Up @@ -51,6 +52,7 @@ pub struct RoundContext {
pub collapsed_tools: Vec<String>,
pub unlocked_collapsed_tools: Vec<String>,
pub model_name: String,
pub primary_model_facts: PrimaryModelFacts,
pub agent_type: String,
pub context_vars: HashMap<String, String>,
pub(crate) delegation_policy: DelegationPolicy,
Expand Down
3 changes: 0 additions & 3 deletions src/crates/assembly/core/src/agentic/skill_agent_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ pub async fn resolve_skill_agent_snapshot(
workspace,
workspace_services,
None,
None,
None,
true,
context_vars,
);
let manifest = resolve_tool_manifest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ mod tests {
dialog_turn_id: Some("turn-1".to_string()),
workspace: Some(WorkspaceBinding::new(None, root)),
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data: HashMap::new(),
computer_use_host: None,
runtime_tool_restrictions: Default::default(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ mod tests {
dialog_turn_id: None,
workspace: None,
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data,
computer_use_host: None,
runtime_tool_restrictions: Default::default(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,7 @@ mod tests {
dialog_turn_id: None,
workspace: None,
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data: HashMap::new(),
computer_use_host: None,
runtime_tool_restrictions: Default::default(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,14 +448,6 @@ The **primary model cannot consume images** in tool results — **do not** use *
Ok(vec![ToolResult::ok(body, Some(hint.to_string()))])
}

fn primary_api_format(ctx: &ToolUseContext) -> String {
ctx.custom_data
.get("primary_model_provider")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_lowercase()
}

/// Screenshot tool results attach JPEGs via `tool_image_attachments`; only providers whose
/// request converters emit multimodal tool output are supported (Anthropic + OpenAI-compatible).
fn require_multimodal_tool_output_for_screenshot(ctx: &ToolUseContext) -> BitFunResult<()> {
Expand All @@ -464,11 +456,7 @@ The **primary model cannot consume images** in tool results — **do not** use *
"The primary model does not accept images; do not use ComputerUse action `screenshot` or other image-producing steps. Use `click_element`, `locate`, `move_to_text` (with `move_to_text_match_index` when listed), `mouse_move` with globals from tool JSON, `key_chord`, etc.".to_string(),
));
}
let f = Self::primary_api_format(ctx);
if matches!(
f.as_str(),
"anthropic" | "openai" | "response" | "responses"
) {
if ctx.primary_model_facts().multimodal_tool_output_supported() {
return Ok(());
}
Err(BitFunError::tool(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2066,6 +2066,7 @@ mod control_hub_tests {
dialog_turn_id: None,
workspace: None,
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data: std::collections::HashMap::new(),
computer_use_host: None,
runtime_tool_restrictions: Default::default(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,7 @@ mod tests {
dialog_turn_id: None,
workspace: None,
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data: HashMap::new(),
computer_use_host: None,
runtime_tool_restrictions: Default::default(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,7 @@ mod tests {
session_identity,
)),
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data: HashMap::new(),
computer_use_host: None,
runtime_tool_restrictions: ToolRuntimeRestrictions::default(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ mod tests {
dialog_turn_id: None,
workspace: Some(WorkspaceBinding::new(None, root)),
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data: HashMap::new(),
computer_use_host: None,
runtime_tool_restrictions: ToolRuntimeRestrictions::default(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ mod tests {
dialog_turn_id: None,
workspace: None,
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data: HashMap::new(),
computer_use_host: None,
runtime_tool_restrictions: Default::default(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ mod tests {
dialog_turn_id: None,
workspace: None,
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data: HashMap::new(),
computer_use_host: None,
runtime_tool_restrictions: Default::default(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ mod tests {
dialog_turn_id: None,
workspace: None,
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data: HashMap::new(),
computer_use_host: None,
runtime_tool_restrictions: Default::default(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ Use the remote project skill.
dialog_turn_id: None,
workspace: Some(workspace),
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data: Default::default(),
computer_use_host: None,
runtime_tool_restrictions: Default::default(),
Expand Down Expand Up @@ -449,6 +450,7 @@ Use the remote project skill.
dialog_turn_id: None,
workspace: Some(workspace),
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data: Default::default(),
computer_use_host: None,
runtime_tool_restrictions: Default::default(),
Expand Down Expand Up @@ -496,6 +498,7 @@ Use the remote project skill.
dialog_turn_id: None,
workspace: None,
unlocked_collapsed_tools: Vec::new(),
primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(),
custom_data: Default::default(),
computer_use_host: None,
runtime_tool_restrictions: Default::default(),
Expand Down
Loading
Loading