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) const WINDOWS_SHELL_HINT: &str = "The shell tool runs on Windows through `cmd.exe /C`, \
not a POSIX shell. GNU coreutils are not available: use `type` or PowerShell's `Get-Content` \
instead of `cat`, `findstr` or `Select-String` instead of `grep`, `dir` or `Get-ChildItem` \
instead of `ls`, and `Measure-Object -Line` instead of `wc -l`. Run a PowerShell command as \
`powershell -Command \"...\"`. Paths use backslashes and drive letters, and `%VAR%` (cmd) or \
`$env:VAR` (PowerShell) expands environment variables.";
pub(crate) fn shell_guidance_for(os: &str) -> Option<&'static str> {
match os {
"windows" => Some(WINDOWS_SHELL_HINT),
_ => None,
}
}
pub(crate) fn hint_blocks(
config: Option<&InferenceConfig>,
tools: &[Tool],
os: &str,
) -> Vec<leviath_providers::SystemBlock> {
let always = |text: &str| leviath_providers::SystemBlock {
text: text.to_string(),
cache_hint: leviath_core::CacheHint::Always,
};
let mut blocks = Vec::new();
if config.map(|c| c.batch_tool_hint).unwrap_or(false) {
blocks.push(always(BATCH_TOOL_HINT));
}
if config.map(|c| c.shell_hint).unwrap_or(false)
&& tools.iter().any(|t| t.name == "shell")
&& let Some(text) = shell_guidance_for(os)
{
blocks.push(always(text));
}
blocks
}
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 = hint_blocks(config, &filtered_tools, std::env::consts::OS);
system.extend(assembled.system_blocks);
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>,
Option<&DispatchStall>,
),
With<ReadyToInfer>,
>,
stage: Res<InferenceStage>,
providers: Res<Providers>,
circuits: Option<Res<ProviderCircuits>>,
policy: Option<Res<CircuitPolicy>>,
par_commands: ParallelCommands,
) {
crate::tick_scope::clear();
let now = chrono::Utc::now().timestamp();
let circuit_policy = policy.map(|p| *p).unwrap_or_default();
let circuits = circuits.as_deref();
agents.par_iter().for_each(
|(entity, state, window, config, si, in_flight, progress, stalled)| {
crate::tick_scope::run_agent_parallel(entity, &par_commands, &mut || {
if state.status != AgentStatus::Active {
return; }
let stall = |reason| {
let noted = note_stall(stalled, reason, now);
par_commands.command_scope(|mut commands| {
commands.entity(entity).insert(noted);
});
};
if circuits.is_some_and(|c| c.is_open(&si.provider_name, now, &circuit_policy)) {
tracing::debug!(
provider = %si.provider_name,
"inference waiting: the provider's circuit is open"
);
stall(StallReason::ProviderCircuitOpen);
return;
}
let Some(provider) = providers.0.get(&si.provider_name) else {
tracing::warn!(
provider = %si.provider_name,
"inference waiting: provider not registered"
);
stall(StallReason::ProviderMissing);
return;
};
let Some(permit) = stage.pools.try_acquire(&si.model) else {
tracing::debug!(
model = %si.model,
"inference waiting: per-model pool is full"
);
stall(StallReason::PoolFull);
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();
let lost_outcomes = stage.outcomes.clone();
let lost_wake = stage.wake.clone();
crate::lane_supervisor::spawn_supervised(
&stage.runtime,
"inference",
run_inference_job(
job,
stage.outcomes.clone(),
stage.wake.clone(),
retry_policy_for(config),
cancel.clone(),
),
move |message| {
let _ = lost_outcomes.send(InferenceOutcome {
entity,
result: Err(leviath_providers::ProviderError::Other(message)),
latency: std::time::Duration::ZERO,
});
lost_wake.notify_one();
},
);
par_commands.command_scope(|mut commands| {
track_in_flight(&mut commands, entity, in_flight, cancel);
commands
.entity(entity)
.remove::<ReadyToInfer>()
.remove::<DispatchStall>()
.insert(AwaitingInference);
});
});
},
);
}