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