use aion::Engine;
use aion_integration_acp::catalogue;
use serde::Serialize;
use serde_json::Value;
use super::document::{
CONTINUE_END_FIELD, CONTINUE_MESSAGE_FIELD, CONTINUE_SIGNAL, EMBEDDED_ASSISTANT_FILENAME,
EmbeddedAssistant, OBJECTIVE_INPUT, REPO_PATH_INPUT, STATUS_QUERY,
};
use super::sessions::{AssistantSessionError, AssistantSessions};
use crate::namespace::CallerIdentity;
use crate::namespace::grants::GRANT_WORDS;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case", tag = "state")]
pub enum AssistantResidency {
Routed,
LoadedNotRouted {
routed_hash: Option<String>,
},
NotLoaded {
routed_hash: Option<String>,
},
Unknown {
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantSessionContract {
pub objective_input: &'static str,
pub repo_path_input: &'static str,
pub continue_signal: &'static str,
pub message_field: &'static str,
pub end_field: &'static str,
pub status_query: &'static str,
}
impl AssistantSessionContract {
#[must_use]
pub const fn current() -> Self {
Self {
objective_input: OBJECTIVE_INPUT,
repo_path_input: REPO_PATH_INPUT,
continue_signal: CONTINUE_SIGNAL,
message_field: CONTINUE_MESSAGE_FIELD,
end_field: CONTINUE_END_FIELD,
status_query: STATUS_QUERY,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantSignal {
pub name: String,
pub input_schema: Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantHarnessDescriptor {
pub name: String,
pub kind: String,
pub accounts: Vec<String>,
pub available: bool,
pub install_hint: Option<String>,
pub launch: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantToolsDescriptor {
pub aion: bool,
pub assistant: AssistantOwnToolsDescriptor,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantOwnToolsDescriptor {
pub server: String,
pub route: String,
pub tools: Vec<String>,
pub handed_over: bool,
pub unavailable_reason: Option<String>,
pub token: AssistantSessionTokenDescriptor,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantSessionTokenDescriptor {
pub kind: String,
pub minted_by: String,
pub scope: String,
pub description: String,
}
impl AssistantSessionTokenDescriptor {
#[must_use]
pub fn current() -> Self {
Self {
kind: crate::assistant::mcp::SESSION_TOKEN_KIND.to_owned(),
minted_by: TOKEN_MINTED_BY.to_owned(),
scope: TOKEN_SCOPE.to_owned(),
description: crate::assistant::mcp::SESSION_TOKEN_DESCRIPTION.to_owned(),
}
}
}
const TOKEN_MINTED_BY: &str = "server";
const TOKEN_SCOPE: &str = "session";
const NO_DIALABLE_ADDRESS: &str = "this server cannot state an address an agent could dial back on (`server.listen_address` \
names port 0, whose real port is only known after bind), so no MCP server of ours is handed \
to a session's agent and it cannot read what is on the operator's screen";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantGrantDescriptor {
pub name: String,
pub held: bool,
pub description: String,
}
const HARNESS_KIND: &str = "acp";
const ASSISTANT_TOOL_SERVER_NAME: &str = "assistant";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantDescriptor {
pub workflow_type: String,
pub task_queue: String,
pub content_hash: String,
pub document_filename: &'static str,
pub document_bytes: usize,
pub input_schema: Value,
pub signals: Vec<AssistantSignal>,
pub queries: Vec<String>,
pub session: AssistantSessionContract,
pub residency: AssistantResidency,
pub harnesses: Vec<AssistantHarnessDescriptor>,
pub default_harness: Option<String>,
pub tools: AssistantToolsDescriptor,
pub sessions_enabled: bool,
pub sessions_disabled_reason: Option<String>,
pub grants: Vec<AssistantGrantDescriptor>,
}
pub async fn describe(
embedded: &EmbeddedAssistant,
engine: &Engine,
sessions: &AssistantSessions,
caller: &CallerIdentity,
) -> Result<AssistantDescriptor, AssistantSessionError> {
let config = sessions.config();
let availability = sessions.availability();
let default_harness = sessions.last_harness_pick(caller.subject()).await?;
Ok(AssistantDescriptor {
workflow_type: embedded.workflow_type().to_owned(),
task_queue: embedded.task_queue().to_owned(),
content_hash: embedded.content_hash().to_string(),
document_filename: EMBEDDED_ASSISTANT_FILENAME,
document_bytes: embedded.source().len(),
input_schema: embedded.input_schema().clone(),
signals: embedded
.signals()
.iter()
.map(|signal| AssistantSignal {
name: signal.name.clone(),
input_schema: signal.input_schema.clone(),
})
.collect(),
queries: embedded.queries().to_vec(),
session: AssistantSessionContract::current(),
residency: residency(embedded, engine),
harnesses: catalogue::CATALOGUE
.iter()
.map(|entry| AssistantHarnessDescriptor {
name: entry.id.to_owned(),
kind: HARNESS_KIND.to_owned(),
accounts: config.account_names(entry.id),
available: entry.available(),
install_hint: (!entry.available()).then(|| entry.install_hint.to_owned()),
launch: entry.launch(),
})
.collect(),
default_harness,
tools: AssistantToolsDescriptor {
aion: sessions.hands_over_general_mcp(),
assistant: AssistantOwnToolsDescriptor {
server: ASSISTANT_TOOL_SERVER_NAME.to_owned(),
route: crate::assistant::mcp::ASSISTANT_MCP_PATH.to_owned(),
tools: crate::assistant::mcp::SESSION_TOOL_NAMES
.iter()
.map(|name| (*name).to_owned())
.collect(),
handed_over: sessions.hands_over_assistant_tools(),
unavailable_reason: (!sessions.hands_over_assistant_tools())
.then(|| NO_DIALABLE_ADDRESS.to_owned()),
token: AssistantSessionTokenDescriptor::current(),
},
},
sessions_enabled: availability.is_available(),
sessions_disabled_reason: availability.reason().map(ToOwned::to_owned),
grants: GRANT_WORDS
.iter()
.map(|grant| AssistantGrantDescriptor {
name: grant.word().to_owned(),
held: grant.granted_for(caller),
description: grant.description().to_owned(),
})
.collect(),
})
}
fn residency(embedded: &EmbeddedAssistant, engine: &Engine) -> AssistantResidency {
let versions = match engine.list_workflow_versions() {
Ok(versions) => versions,
Err(error) => {
return AssistantResidency::Unknown {
reason: format!("the engine catalog could not be read: {error}"),
};
}
};
let embedded_hash = embedded.content_hash().to_string();
let resident: Vec<_> = versions
.into_iter()
.filter(|version| version.workflow_type == embedded.workflow_type())
.collect();
let loaded = resident
.iter()
.any(|version| version.content_hash.to_string() == embedded_hash);
let routed_hash = resident
.iter()
.find(|version| version.route_active)
.map(|version| version.content_hash.to_string());
if !loaded {
return AssistantResidency::NotLoaded { routed_hash };
}
if routed_hash.as_deref() == Some(embedded_hash.as_str()) {
AssistantResidency::Routed
} else {
AssistantResidency::LoadedNotRouted { routed_hash }
}
}