use super::*;
pub(crate) const BATCH_TOOL_HINT: &str = "You can call multiple tools in a single response. \
When operations are independent (reading, editing, or writing different files, or \
writing a file then running a command that doesn't need its output), batch them in \
one response to cut round trips. Do NOT batch when a call depends on a previous \
call's result, or when you must see a command's output before deciding the next step.";
pub(crate) fn build_request(
window: &ContextWindow,
config: Option<&InferenceConfig>,
stage: &StageInference,
provider: &Arc<dyn Provider>,
stage_name: &str,
stage_iterations: usize,
) -> InferenceRequest {
let assembled = window.assemble_with_meta(&crate::custom_region::AssembleMeta {
stage_name: stage_name.to_string(),
stage_iterations,
model: stage.model.clone(),
});
let remaining = window.max_tokens.saturating_sub(window.current_tokens);
let caps = provider.capabilities(&stage.model);
let output_cap = config
.and_then(|c| c.max_output_tokens)
.unwrap_or(caps.max_output_tokens);
let max_tokens = remaining.min(output_cap);
let filtered_tools = match stage.tool_filter.as_deref() {
Some(filter) if !filter.is_empty() => stage
.tools
.iter()
.filter(|t| filter.iter().any(|f| f == &t.name))
.cloned()
.collect(),
_ => stage.tools.clone(),
};
let temperature = if caps.supports_temperature {
config.and_then(|c| c.temperature).unwrap_or(0.7)
} else {
0.0
};
let extra = match config.map(|c| &c.extra_params) {
Some(params) if !params.is_empty() => serde_json::Value::Object(params.clone()),
_ => serde_json::Value::Null,
};
let mut system = assembled.system_blocks;
if config.map(|c| c.batch_tool_hint).unwrap_or(false) {
system.insert(
0,
leviath_providers::SystemBlock {
text: BATCH_TOOL_HINT.to_string(),
cache_hint: leviath_core::CacheHint::Always,
},
);
}
InferenceRequest {
system,
messages: assembled.messages,
model: stage.model.clone(),
max_tokens,
temperature,
tools: filtered_tools,
extra,
request_timeout_secs: config.and_then(|c| c.request_timeout_secs),
}
}
pub(crate) fn retry_policy_for(
config: Option<&InferenceConfig>,
) -> crate::inference_bridge::RetryPolicy {
let mut policy = crate::inference_bridge::RetryPolicy::default();
if let Some(secs) = config.and_then(|c| c.request_timeout_secs) {
policy.job_timeout = std::time::Duration::from_secs(secs);
}
policy
}
#[derive(Component, Default, Debug)]
pub struct InFlightWork(pub Vec<crate::cancel::CancelToken>);
pub fn abort_terminal_work(
agents: Query<(Entity, &AgentState, &InFlightWork)>,
mut commands: Commands,
) {
crate::tick_scope::clear();
for (entity, state, in_flight) in agents.iter() {
if !is_terminal_status(&state.status) {
continue;
}
crate::tick_scope::enter(entity);
for token in &in_flight.0 {
token.cancel();
}
commands.entity(entity).remove::<InFlightWork>();
}
}
pub(crate) fn track_in_flight(
commands: &mut Commands,
entity: Entity,
existing: Option<&InFlightWork>,
token: crate::cancel::CancelToken,
) {
let mut tokens = existing.map(|w| w.0.clone()).unwrap_or_default();
tokens.push(token);
commands.entity(entity).insert(InFlightWork(tokens));
}
#[allow(clippy::type_complexity)]
pub fn dispatch_inference(
agents: Query<
(
Entity,
&AgentState,
&ContextWindow,
Option<&InferenceConfig>,
&StageInference,
Option<&InFlightWork>,
Option<&StageProgress>,
),
With<ReadyToInfer>,
>,
stage: Res<InferenceStage>,
providers: Res<Providers>,
par_commands: ParallelCommands,
) {
crate::tick_scope::clear();
agents
.par_iter()
.for_each(|(entity, state, window, config, si, in_flight, progress)| {
crate::tick_scope::run_agent_parallel(entity, &par_commands, &mut || {
if state.status != AgentStatus::Active {
return; }
let Some(provider) = providers.0.get(&si.provider_name) else {
tracing::warn!(
provider = %si.provider_name,
"inference waiting: provider not registered"
);
return;
};
let Some(permit) = stage.pools.try_acquire(&si.model) else {
tracing::debug!(
model = %si.model,
"inference waiting: per-model pool is full"
);
return;
};
let request = build_request(
window,
config,
si,
&provider,
&state.current_stage,
progress.map(|p| p.iterations).unwrap_or(0),
);
let job = InferenceJob {
entity,
provider,
request,
permit,
exact_token_counting: stage.exact_token_counting,
};
let cancel = crate::cancel::CancelToken::new();
stage.runtime.spawn(run_inference_job(
job,
stage.outcomes.clone(),
stage.wake.clone(),
retry_policy_for(config),
cancel.clone(),
));
par_commands.command_scope(|mut commands| {
track_in_flight(&mut commands, entity, in_flight, cancel);
commands
.entity(entity)
.remove::<ReadyToInfer>()
.insert(AwaitingInference);
});
});
});
}