use aion_mcp::tasks::resolve::{TaskProjection, TaskResolveError};
use aion_mcp::tools::service::{
CatalogError, ToolCall, ToolCatalog, ToolFailure, ToolOutcome, ToolService, ToolTaskDecision,
};
use serde_json::json;
use crate::ServerState;
use super::caller::AssistantSessionCaller;
use super::catalog::{
ASSISTANT_CONTEXT_TOOL, ASSISTANT_DOCUMENT_CHECK_TOOL, ASSISTANT_DOCUMENT_EDIT_TOOL,
assistant_tool_catalog,
};
use super::context_tool;
use super::document_check_tool;
use super::document_edit_tool;
pub(crate) struct AssistantToolService {
state: ServerState,
catalog: ToolCatalog,
instructions: String,
}
impl AssistantToolService {
pub(crate) fn new(state: ServerState) -> Result<Self, CatalogError> {
Ok(Self {
state,
catalog: assistant_tool_catalog()?,
instructions: INSTRUCTIONS.to_owned(),
})
}
}
#[async_trait::async_trait]
impl ToolService for AssistantToolService {
type Caller = AssistantSessionCaller;
fn catalog(&self) -> &ToolCatalog {
&self.catalog
}
fn instructions(&self) -> &str {
&self.instructions
}
fn task_decision(&self, _call: &ToolCall) -> ToolTaskDecision {
ToolTaskDecision::Inline
}
async fn call(
&self,
caller: &Self::Caller,
call: ToolCall,
) -> Result<ToolOutcome, ToolFailure> {
match call.name.as_str() {
ASSISTANT_CONTEXT_TOOL => {
context_tool::assistant_context(self.state.assistant_sessions(), caller).await
}
ASSISTANT_DOCUMENT_EDIT_TOOL => {
document_edit_tool::assistant_document_edit(
self.state.assistant_sessions(),
caller,
serde_json::Value::Object(call.arguments),
)
.await
}
ASSISTANT_DOCUMENT_CHECK_TOOL => {
document_check_tool::assistant_document_check(&self.state, caller).await
}
other => Err(ToolFailure::new(
format!(
"`{other}` is not a tool on the assistant route. This route publishes \
`{ASSISTANT_CONTEXT_TOOL}`, `{ASSISTANT_DOCUMENT_EDIT_TOOL}` and \
`{ASSISTANT_DOCUMENT_CHECK_TOOL}`; the workflow tools are on this server's \
general MCP endpoint, under a different credential."
),
json!({ "code": "unknown_tool", "tool": other }),
)),
}
}
fn task_handle(&self, _call: &ToolCall, _outcome: &ToolOutcome) -> Option<String> {
None
}
async fn resolve_task(
&self,
_caller: &Self::Caller,
task_id: &str,
) -> Result<TaskProjection, TaskResolveError> {
Err(TaskResolveError::Unknown {
task_id: task_id.to_owned(),
})
}
}
const INSTRUCTIONS: &str = "This endpoint belongs to ONE assistant conversation — the one whose bearer you called with. \
It publishes the editing loop, three tools about that conversation and nothing else.\n\n\
`assistant_context` answers what the operator has on screen in the Aion console right now: \
the page, the concepts that page explains, and the document their editor is showing with \
any selection in it. Call it before asking the operator where anything is. It returns the \
document's path, so you never need to ask which directory a repository is in or which file \
is open — asking for either is asking for something you were already given.\n\n\
`assistant_document_edit` edits that document in place, as replace-exactly-once operations \
quoted from what you read. The edits land in the operator's editor as you make them, and \
the operator keeps or reverts them — so make small, named changes one concept at a time, \
and never read or save files to reach this document: it is the operator's buffer, and \
saving stays the operator's act.\n\n\
`assistant_document_check` runs the editor's own AWL check over that document as it now \
stands, your edits included. Check after you edit and fix what it reports before telling \
the operator what you changed.\n\n\
None of these name a session or a file: your credential already names the conversation, and \
there is no way through this endpoint to reach another one. This server's workflow tools \
are NOT here; they are on its general MCP endpoint under a different credential.";
#[cfg(test)]
mod tests {
use super::super::catalog::ASSISTANT_CONTEXT_TOOL;
use super::INSTRUCTIONS;
#[test]
fn the_instructions_tell_the_agent_not_to_ask_where_the_repository_is() {
assert!(INSTRUCTIONS.contains(ASSISTANT_CONTEXT_TOOL));
assert!(
INSTRUCTIONS.contains("which directory a repository is in"),
"the instruction that stops the agent asking for the repo path is load-bearing"
);
}
}