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 },
65 "additionalProperties": false
66 }
67 }
68 },
69 "additionalProperties": false
70 }
71 }
72 ]);
73 if let Some(entries) = table.as_array_mut() {
74 entries.extend(crate::network::lifecycle::tool_entries());
75 entries.extend(super::subscriptions::tool_entries());
76 entries.extend(super::listen::entries());
77 entries.extend(super::plan_tools::tool_entries());
78 entries.extend(super::template_tools::tool_entries());
79 entries.push(super::channel_send::entry());
80 }
81 table
82}
83
84#[derive(Clone, PartialEq, Eq)]
86pub struct ToolResult {
87 pub structured: Value,
89 pub is_error: bool,
91}
92
93impl ToolResult {
94 #[must_use]
96 pub const fn ok(structured: Value) -> Self {
97 Self {
98 structured,
99 is_error: false,
100 }
101 }
102
103 #[must_use]
105 pub fn failed(words: &str, structured: Value) -> Self {
106 let mut structured = structured;
107 if let Some(object) = structured.as_object_mut() {
108 object.insert("error".to_owned(), json!(words));
109 }
110 Self {
111 structured,
112 is_error: true,
113 }
114 }
115
116 pub(super) fn into_result(self, era: Era) -> Value {
117 let text = self
118 .structured
119 .get("error")
120 .and_then(Value::as_str)
121 .map_or_else(
122 || serde_json::to_string(&self.structured).unwrap_or_default(),
123 str::to_owned,
124 );
125 let text = super::held::text(&self).unwrap_or(text);
126 let mut result = Map::new();
127 result.insert(
128 "content".to_owned(),
129 json!([{ "type": "text", "text": text }]),
130 );
131 result.insert("structuredContent".to_owned(), self.structured);
132 result.insert("isError".to_owned(), json!(self.is_error));
133 shaped(era, result)
134 }
135}
136
137pub(super) fn string<'a>(
138 arguments: &'a Map<String, Value>,
139 field: &str,
140) -> Result<&'a str, Refusal> {
141 arguments
142 .get(field)
143 .and_then(Value::as_str)
144 .ok_or_else(|| Refusal {
145 code: INVALID_PARAMS,
146 message: format!("the tool call is missing {field}"),
147 data: None,
148 })
149}
150
151pub(super) fn object<'a>(
152 arguments: &'a Map<String, Value>,
153 field: &str,
154) -> Result<&'a Value, Refusal> {
155 match arguments.get(field) {
156 Some(value @ Value::Object(_)) => Ok(value),
157 _ => Err(Refusal {
158 code: INVALID_PARAMS,
159 message: format!("the tool call's {field} is missing or not an object"),
160 data: None,
161 }),
162 }
163}