pub const PRIORITY_MODE_ENV: &str = "AXTERMINATOR_PRIORITY_MODE";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourcePriorityMode {
Legacy,
Explicit,
}
impl SourcePriorityMode {
#[must_use]
pub fn from_env() -> Self {
Self::from_raw(std::env::var(PRIORITY_MODE_ENV).ok().as_deref())
}
#[must_use]
pub fn from_raw(raw: Option<&str>) -> Self {
match raw.map(str::trim) {
Some("explicit") => Self::Explicit,
Some("legacy") | None => Self::Legacy,
Some(_) => Self::Legacy,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Legacy => "legacy",
Self::Explicit => "explicit",
}
}
#[must_use]
pub const fn is_explicit(self) -> bool {
matches!(self, Self::Explicit)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstructionSource {
HumanUserPrompt,
AgentToolArgs,
HumanToolArgs,
AxApi,
AppDialog,
ScreenVision,
}
impl InstructionSource {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::HumanUserPrompt => "human_user_prompt",
Self::AgentToolArgs => "agent_tool_args",
Self::HumanToolArgs => "human_tool_args",
Self::AxApi => "ax_api",
Self::AppDialog => "app_dialog",
Self::ScreenVision => "screen_vision",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvocationActor {
Human,
Agent,
}
impl InvocationActor {
#[must_use]
pub fn from_tool_arg(raw: Option<&str>) -> Self {
match raw {
Some("human") => Self::Human,
Some("agent") | None => Self::Agent,
Some(_) => Self::Agent,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Human => "human",
Self::Agent => "agent",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceCandidate {
pub source: InstructionSource,
pub value: String,
}
impl SourceCandidate {
#[must_use]
pub fn new(source: InstructionSource, value: impl Into<String>) -> Self {
Self {
source,
value: value.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceDecision {
pub source: InstructionSource,
pub value: String,
pub overridden_source: Option<InstructionSource>,
pub reason: &'static str,
}
impl SourceDecision {
#[must_use]
pub const fn overrode_tool_args(&self) -> bool {
matches!(
self.overridden_source,
Some(InstructionSource::AgentToolArgs)
)
}
}
#[must_use]
pub fn select_effective_description(
tool_description: &str,
user_prompt: Option<&str>,
actor: InvocationActor,
) -> SourceDecision {
let tool_value = tool_description.trim();
let prompt_value = user_prompt.map(str::trim).filter(|value| !value.is_empty());
match (actor, prompt_value) {
(InvocationActor::Agent, Some(prompt)) => {
let conflicts = !same_instruction(prompt, tool_value);
SourceDecision {
source: InstructionSource::HumanUserPrompt,
value: prompt.to_string(),
overridden_source: conflicts.then_some(InstructionSource::AgentToolArgs),
reason: if conflicts {
"human user prompt has higher authority than agent-supplied tool args"
} else {
"human user prompt and agent tool args agree"
},
}
}
(InvocationActor::Agent, None) => SourceDecision {
source: InstructionSource::AgentToolArgs,
value: tool_value.to_string(),
overridden_source: None,
reason: "no separate initiating user prompt was supplied",
},
(InvocationActor::Human, _) => SourceDecision {
source: InstructionSource::HumanToolArgs,
value: tool_value.to_string(),
overridden_source: None,
reason: "direct human tool args are treated as the user instruction",
},
}
}
#[must_use]
pub fn select_legacy_description(tool_description: &str, actor: InvocationActor) -> SourceDecision {
SourceDecision {
source: match actor {
InvocationActor::Human => InstructionSource::HumanToolArgs,
InvocationActor::Agent => InstructionSource::AgentToolArgs,
},
value: tool_description.trim().to_string(),
overridden_source: None,
reason: "legacy mode preserves the supplied tool description",
}
}
#[must_use]
pub fn select_ui_fact(candidates: &[SourceCandidate]) -> Option<SourceDecision> {
let chosen = candidates
.iter()
.filter(|candidate| !candidate.value.trim().is_empty())
.max_by_key(|candidate| ui_fact_rank(candidate.source))?;
let overridden_source = candidates
.iter()
.filter(|candidate| candidate.source != chosen.source)
.filter(|candidate| !candidate.value.trim().is_empty())
.filter(|candidate| !same_instruction(&candidate.value, &chosen.value))
.max_by_key(|candidate| ui_fact_rank(candidate.source))
.map(|candidate| candidate.source);
Some(SourceDecision {
source: chosen.source,
value: chosen.value.trim().to_string(),
overridden_source,
reason: match chosen.source {
InstructionSource::AxApi => {
"AX API facts have higher authority than app-dialog or screen-vision facts"
}
InstructionSource::AppDialog => {
"app-dialog facts have higher authority than screen-vision facts when AX is absent"
}
InstructionSource::ScreenVision => {
"screen vision is used only when no higher UI fact exists"
}
InstructionSource::HumanUserPrompt
| InstructionSource::AgentToolArgs
| InstructionSource::HumanToolArgs => {
"instruction sources are not UI facts; preserve supplied order"
}
},
})
}
fn ui_fact_rank(source: InstructionSource) -> u8 {
match source {
InstructionSource::AxApi => 30,
InstructionSource::AppDialog => 20,
InstructionSource::ScreenVision => 10,
InstructionSource::HumanUserPrompt
| InstructionSource::AgentToolArgs
| InstructionSource::HumanToolArgs => 0,
}
}
fn same_instruction(left: &str, right: &str) -> bool {
normalise(left) == normalise(right)
}
fn normalise(value: &str) -> String {
value
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_lowercase()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ax_api_beats_screen_vision_for_same_field() {
let chosen = select_ui_fact(&[
SourceCandidate::new(InstructionSource::ScreenVision, "Delete"),
SourceCandidate::new(InstructionSource::AxApi, "Cancel"),
])
.expect("candidate selected");
assert_eq!(chosen.source, InstructionSource::AxApi);
assert_eq!(chosen.value, "Cancel");
assert_eq!(
chosen.overridden_source,
Some(InstructionSource::ScreenVision)
);
}
#[test]
fn app_dialog_beats_screen_vision_when_ax_is_missing() {
let chosen = select_ui_fact(&[
SourceCandidate::new(InstructionSource::ScreenVision, "OK"),
SourceCandidate::new(InstructionSource::AppDialog, "Don't Save"),
])
.expect("candidate selected");
assert_eq!(chosen.source, InstructionSource::AppDialog);
assert_eq!(chosen.value, "Don't Save");
}
#[test]
fn user_prompt_overrides_agent_tool_args() {
let chosen = select_effective_description(
"click the delete button",
Some("click the cancel button"),
InvocationActor::Agent,
);
assert_eq!(chosen.source, InstructionSource::HumanUserPrompt);
assert_eq!(chosen.value, "click the cancel button");
assert!(chosen.overrode_tool_args());
}
#[test]
fn human_direct_tool_args_are_the_user_instruction() {
let chosen = select_effective_description(
"click the delete button",
Some("click the cancel button"),
InvocationActor::Human,
);
assert_eq!(chosen.source, InstructionSource::HumanToolArgs);
assert_eq!(chosen.value, "click the delete button");
assert!(!chosen.overrode_tool_args());
}
#[test]
fn priority_mode_defaults_to_legacy() {
assert_eq!(
SourcePriorityMode::from_raw(None),
SourcePriorityMode::Legacy
);
assert_eq!(
SourcePriorityMode::from_raw(Some("unknown")),
SourcePriorityMode::Legacy
);
}
#[test]
fn priority_mode_accepts_explicit_and_legacy() {
assert_eq!(
SourcePriorityMode::from_raw(Some("explicit")),
SourcePriorityMode::Explicit
);
assert_eq!(
SourcePriorityMode::from_raw(Some("legacy")),
SourcePriorityMode::Legacy
);
}
}