blob: 1522d49c3b74efd22d2668f97ec30b798dc42956 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::*;
use maka_skills::{
Catalog, HostCapabilities, Preferences, SkillFailedReceipt, SkillInvocationMode,
SkillInvocationReceipt,
};
fn host(tools: Vec<String>) -> Result<HostCapabilities, String> {
if tools
.iter()
.any(|name| name.is_empty() || name.chars().any(char::is_control))
{
return Err("skills_tool_context_invalid".into());
}
Ok(HostCapabilities {
tools: tools.into_iter().collect(),
capabilities: Default::default(),
})
}
// Page toward the archive preview target; a complete first item can exceed it.
fn fits(value: &impl serde::Serialize) -> bool {
serde_json::to_string(value)
.is_ok_and(|text| text.encode_utf16().count() <= maka_agent_facts::read::MAX_PAGE_CHARS)
}
impl Library {
pub(super) async fn model(
&self,
method: &str,
input: Value,
scope: &Scope,
) -> Result<Value, String> {
let request: SkillToolRequest<Value> =
serde_json::from_value(input).map_err(|_| "skills_query_invalid")?;
let library = self.for_workspace(request.workspace, scope).await?;
let host = host(request.tools)?;
let discovery = library.discover(scope).await?;
let preferences = self.preferences.read(scope).await?;
let preference_revision = preferences.revision;
let preferences = Preferences::Available(preferences.entries);
let catalog = Catalog {
discovery: &discovery,
preferences: &preferences,
host: &host,
};
let revision = maka_agent_facts::artifact::content_digest(
&serde_json::to_vec(&(
&library.workspace,
catalog
.fingerprint()
.map_err(|_| "skills_encoding_failed")?,
preference_revision,
))
.map_err(|_| "skills_encoding_failed")?,
);
match method {
"prepare" => {
let input: Prepare =
serde_json::from_value(request.input).map_err(|_| "skills_query_invalid")?;
super::preparation::prepare(&catalog, revision, input)
}
"search" => {
let input: ModelSearch =
serde_json::from_value(request.input).map_err(|_| "skills_query_invalid")?;
if input.limit.is_some_and(|n| !(1..=8).contains(&n)) {
return Err("skills_query_invalid".into());
}
let mut page = ModelSearchResult {
revision,
result: catalog.search(&input.query, input.limit.unwrap_or(8)),
};
while page.result.matches.len() > 1 && !fits(&page) {
page.result.matches.pop();
page.result.truncated = true;
}
encode(page)
}
"load" => {
let input: ModelLoad =
serde_json::from_value(request.input).map_err(|_| "skills_query_invalid")?;
if input.revision != revision {
return encode(ModelLoadResult::Changed);
}
let loaded = match catalog.load(&input.name) {
Ok(loaded) => loaded,
Err(reason) => {
return encode(ModelLoadResult::Unavailable {
receipt: SkillInvocationReceipt::Failed(SkillFailedReceipt {
invocation: SkillInvocationMode::ModelTool,
request: if input.name.is_empty() {
"[invalid]".into()
} else {
input.name.clone()
},
reason: reason.clone(),
}),
reason,
});
}
};
if input.resource.is_some() && input.instruction_offset.is_some() {
return Err("skills_query_invalid".into());
}
if let Some(resource) = input.resource {
let reference = &loaded.skill.location.reference;
let mut budget = maka_agent_facts::read::MAX_PAGE_CHARS;
loop {
let (path, page) = match loaded
.skill
.resources
.page_with_budget(reference, &resource, budget)
{
Ok(page) => page,
Err(reason) => {
return encode(ModelLoadResult::ResourceUnavailable { reason });
}
};
let next = page.next.map(|resource| ModelLoad {
name: reference.clone(),
revision: revision.clone(),
resource: Some(resource),
instruction_offset: None,
});
let result = ModelLoadResult::Resource {
revision: revision.clone(),
reference: reference.clone(),
path,
content: page.content,
offset: page.offset,
returned_lines: page.returned_lines,
total_lines: page.total_lines,
partial_line: page.partial_line,
next,
receipt: loaded.receipt(SkillInvocationMode::ModelTool, &input.name),
};
if fits(&result) || budget <= 1 {
return encode(result);
}
budget /= 2;
}
}
let declared = &loaded.skill.document.manifest.attributes.allowed_tools;
let hints: Vec<_> = declared
.iter()
.take(16)
.map(|tool| bound(tool, 128))
.collect();
let hints_truncated = hints.len() != declared.len()
|| hints.iter().zip(declared).any(|(a, b)| a != b);
let offset = input.instruction_offset.unwrap_or(0);
let Some(remainder) = loaded.instructions.get(offset..) else {
return Err("skills_query_invalid".into());
};
let mut instructions = remainder.to_owned();
loop {
let next = (instructions.len() < remainder.len()).then(|| ModelLoad {
name: loaded.skill.location.reference.clone(),
revision: revision.clone(),
resource: None,
instruction_offset: Some(offset + instructions.len()),
});
let result = ModelLoadResult::Loaded {
revision: revision.clone(),
metadata: loaded.skill.into(),
instructions: instructions.clone(),
instruction_offset: offset,
next,
relative_path: loaded.relative_path.to_string_lossy().into_owned(),
declared_tools: hints.clone(),
declared_tools_truncated: hints_truncated,
truncated: loaded.truncated,
receipt: loaded.receipt(SkillInvocationMode::ModelTool, &input.name),
};
if fits(&result) && (!instructions.is_empty() || remainder.is_empty()) {
return encode(result);
}
if instructions.chars().count() <= 1 {
return encode(result);
}
let cut = instructions.len() / 2;
let end = instructions
.floor_char_boundary(cut)
.max(instructions.chars().next().map_or(0, char::len_utf8));
instructions.truncate(end);
}
}
_ => Err("skills_method_unavailable".into()),
}
}
}
#[cfg(test)]
mod tests;