1use serde_json::{Map, Value, json};
4
5use super::framing::INVALID_PARAMS;
6use super::negotiate::{Era, Refusal, shaped};
7pub use super::server::Server;
8
9pub(super) const TOOLS_TTL_MS: u64 = 60_000;
11
12#[must_use]
14pub fn tool_table() -> Value {
15 let mut table = json!([
16 {
17 "name": "list",
18 "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.",
19 "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false }
20 },
21 {
22 "name": "describe",
23 "description": "One subject's station and its activities with the exact input and output JSON schemas the worker registered.",
24 "inputSchema": {
25 "type": "object",
26 "required": ["subject"],
27 "properties": { "subject": { "type": "string", "description": "the subject a station serves" } },
28 "additionalProperties": false
29 }
30 },
31 {
32 "name": "call",
33 "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.",
34 "inputSchema": {
35 "type": "object",
36 "required": ["subject", "input"],
37 "properties": {
38 "subject": { "type": "string" },
39 "input": { "type": "object", "description": "the request body the station's input schema describes" },
40 "reply": { "type": "boolean", "default": true },
41 "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" }
42 },
43 "additionalProperties": false
44 }
45 },
46 {
47 "name": "flow",
48 "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.",
49 "inputSchema": {
50 "type": "object",
51 "required": ["steps"],
52 "properties": {
53 "steps": {
54 "type": "array",
55 "minItems": 1,
56 "items": {
57 "type": "object",
58 "required": ["subject", "input"],
59 "properties": {
60 "subject": { "type": "string" },
61 "input": { "type": "object" },
62 "reply": { "type": "boolean", "default": true },
63 "pass_for": { "type": "string" },
64 "keep": { "type": "boolean", "default": true, "description": "false: the reply feeds the next step's $prev but is answered only by its size" }
65 },
66 "additionalProperties": false
67 }
68 },
69 "answer": { "type": "string", "enum": ["all", "last"], "default": "all", "description": "last: answer only the last step's reply, as reply" }
70 },
71 "additionalProperties": false
72 }
73 }
74 ]);
75 if let Some(entries) = table.as_array_mut() {
76 entries.extend(crate::network::lifecycle::tool_entries());
77 entries.extend(super::subscriptions::tool_entries());
78 entries.extend(super::listen::entries());
79 entries.extend(super::plan_tools::tool_entries());
80 entries.extend(super::template_tools::tool_entries());
81 entries.push(super::channel_send::entry());
82 }
83 table
84}
85
86#[derive(Clone, PartialEq, Eq)]
88pub struct ToolResult {
89 pub structured: Value,
91 pub is_error: bool,
93}
94
95impl ToolResult {
96 #[must_use]
98 pub const fn ok(structured: Value) -> Self {
99 Self {
100 structured,
101 is_error: false,
102 }
103 }
104
105 #[must_use]
107 pub fn failed(words: &str, structured: Value) -> Self {
108 let mut structured = structured;
109 if let Some(object) = structured.as_object_mut() {
110 object.insert("error".to_owned(), json!(words));
111 }
112 Self {
113 structured,
114 is_error: true,
115 }
116 }
117
118 pub(super) fn into_result(self, era: Era) -> Value {
119 let text = self
120 .structured
121 .get("error")
122 .and_then(Value::as_str)
123 .map_or_else(
124 || serde_json::to_string(&self.structured).unwrap_or_default(),
125 str::to_owned,
126 );
127 let text = super::held::text(&self).unwrap_or(text);
128 let mut result = Map::new();
129 result.insert(
130 "content".to_owned(),
131 json!([{ "type": "text", "text": text }]),
132 );
133 result.insert("structuredContent".to_owned(), self.structured);
134 result.insert("isError".to_owned(), json!(self.is_error));
135 shaped(era, result)
136 }
137}
138
139pub(super) fn string<'a>(
140 arguments: &'a Map<String, Value>,
141 field: &str,
142) -> Result<&'a str, Refusal> {
143 arguments
144 .get(field)
145 .and_then(Value::as_str)
146 .ok_or_else(|| Refusal {
147 code: INVALID_PARAMS,
148 message: format!("the tool call is missing {field}"),
149 data: None,
150 })
151}
152
153pub(super) fn object<'a>(
154 arguments: &'a Map<String, Value>,
155 field: &str,
156) -> Result<&'a Value, Refusal> {
157 match arguments.get(field) {
158 Some(value @ Value::Object(_)) => Ok(value),
159 _ => Err(Refusal {
160 code: INVALID_PARAMS,
161 message: format!("the tool call's {field} is missing or not an object"),
162 data: None,
163 }),
164 }
165}