use serde_json::{Map, Value, json};
use super::framing::INVALID_PARAMS;
use super::negotiate::{Era, Refusal, shaped};
pub use super::server::Server;
pub(super) const TOOLS_TTL_MS: u64 = 60_000;
#[must_use]
pub fn tool_table() -> Value {
let mut table = json!([
{
"name": "list",
"description": "The stations on the network's map right now: each with its id, name, device, kind, state and, for a worker, the subjects it serves and the names of its activities. A fresh read every time.",
"inputSchema": { "type": "object", "properties": {}, "additionalProperties": false }
},
{
"name": "describe",
"description": "One subject's station and its activities with the exact input and output JSON schemas the worker registered.",
"inputSchema": {
"type": "object",
"required": ["subject"],
"properties": { "subject": { "type": "string", "description": "the subject a station serves" } },
"additionalProperties": false
}
},
{
"name": "call",
"description": "Send a JSON request on a subject and return the station's reply, or the server's refusal in its own words. With reply false the request is sent and nothing is waited for. With reply true, an unserved subject returns no_server carrying the subject with isError true and never returns the request as a reply.",
"inputSchema": {
"type": "object",
"required": ["subject", "input"],
"properties": {
"subject": { "type": "string" },
"input": { "type": "object", "description": "the request body the station's input schema describes" },
"reply": { "type": "boolean", "default": true },
"pass_for": { "type": "string", "description": "an audience, such as haematite: the ticket asks Lys for this seat's own pass for it and puts it in the input as pass" }
},
"additionalProperties": false
}
},
{
"name": "flow",
"description": "Fire a list of calls in order; a step's input may draw on the previous reply with {\"$prev\": \"<JSON pointer>\"} objects (\"\" is the whole reply). Stops at the first failure, naming the step.",
"inputSchema": {
"type": "object",
"required": ["steps"],
"properties": {
"steps": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["subject", "input"],
"properties": {
"subject": { "type": "string" },
"input": { "type": "object" },
"reply": { "type": "boolean", "default": true },
"pass_for": { "type": "string" }
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
}
]);
if let Some(entries) = table.as_array_mut() {
entries.extend(crate::network::lifecycle::tool_entries());
entries.extend(super::subscriptions::tool_entries());
entries.extend(super::listen::entries());
entries.extend(super::plan_tools::tool_entries());
entries.extend(super::template_tools::tool_entries());
entries.push(super::channel_send::entry());
}
table
}
#[derive(Clone, PartialEq, Eq)]
pub struct ToolResult {
pub structured: Value,
pub is_error: bool,
}
impl ToolResult {
#[must_use]
pub const fn ok(structured: Value) -> Self {
Self {
structured,
is_error: false,
}
}
#[must_use]
pub fn failed(words: &str, structured: Value) -> Self {
let mut structured = structured;
if let Some(object) = structured.as_object_mut() {
object.insert("error".to_owned(), json!(words));
}
Self {
structured,
is_error: true,
}
}
pub(super) fn into_result(self, era: Era) -> Value {
let text = self
.structured
.get("error")
.and_then(Value::as_str)
.map_or_else(
|| serde_json::to_string(&self.structured).unwrap_or_default(),
str::to_owned,
);
let text = super::held::text(&self).unwrap_or(text);
let mut result = Map::new();
result.insert(
"content".to_owned(),
json!([{ "type": "text", "text": text }]),
);
result.insert("structuredContent".to_owned(), self.structured);
result.insert("isError".to_owned(), json!(self.is_error));
shaped(era, result)
}
}
pub(super) fn string<'a>(
arguments: &'a Map<String, Value>,
field: &str,
) -> Result<&'a str, Refusal> {
arguments
.get(field)
.and_then(Value::as_str)
.ok_or_else(|| Refusal {
code: INVALID_PARAMS,
message: format!("the tool call is missing {field}"),
data: None,
})
}
pub(super) fn object<'a>(
arguments: &'a Map<String, Value>,
field: &str,
) -> Result<&'a Value, Refusal> {
match arguments.get(field) {
Some(value @ Value::Object(_)) => Ok(value),
_ => Err(Refusal {
code: INVALID_PARAMS,
message: format!("the tool call's {field} is missing or not an object"),
data: None,
}),
}
}