1use std::fmt;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub enum Tool {
25 Send,
27 Peers,
29 Report,
31 Help,
33 MemoryRead,
35 MemoryWrite,
37 Learn,
39 LogbookAppend,
41 Status,
43 Wait,
45}
46
47impl Tool {
48 pub const ALL: [Self; 10] = [
50 Self::Send,
51 Self::Peers,
52 Self::Report,
53 Self::Help,
54 Self::MemoryRead,
55 Self::MemoryWrite,
56 Self::Learn,
57 Self::LogbookAppend,
58 Self::Status,
59 Self::Wait,
60 ];
61
62 #[must_use]
64 pub const fn name(self) -> &'static str {
65 match self {
66 Self::Send => "layover_send",
67 Self::Peers => "layover_peers",
68 Self::Report => "layover_report",
69 Self::Help => "layover_help",
70 Self::MemoryRead => "layover_memory_read",
71 Self::MemoryWrite => "layover_memory_write",
72 Self::Learn => "layover_learn",
73 Self::LogbookAppend => "layover_logbook_append",
74 Self::Status => "layover_status",
75 Self::Wait => "layover_wait",
76 }
77 }
78
79 #[must_use]
81 pub const fn description(self) -> &'static str {
82 match self {
83 Self::Send => {
84 "Send work to another agent. This is the only way work moves, and sending is what \
85 starts the agent you send to -- there is no separate spawn. You may only send to \
86 agents `layover_peers` lists."
87 }
88 Self::Peers => {
89 "List the agents you may send to and what each one is for. Call this before \
90 deciding where work goes rather than guessing at names."
91 }
92 Self::Report => {
93 "Say what you concluded. One headline, then the detail. Write it for somebody who \
94 was not watching and will read only the headline -- this is the account of your \
95 run that survives."
96 }
97 Self::Help => {
98 "Say that something is in the way and you could not get past it. Use this instead \
99 of producing a plausible answer you do not believe: a wrong answer nobody flags \
100 travels downstream, and that is the failure this exists to prevent."
101 }
102 Self::MemoryRead => {
103 "Read your own notes in full. Every run starts fresh, so this is the only thing \
104 you remember. The most recent part is already in your instructions."
105 }
106 Self::MemoryWrite => {
107 "Add to your own notes, for future runs of you. Nothing else carries over, so \
108 anything worth remembering has to be written here deliberately."
109 }
110 Self::Learn => {
111 "Propose something future runs of you should know. It applies immediately and \
112 lapses unless later runs arrive at it independently."
113 }
114 Self::LogbookAppend => {
115 "Add to the factory's shared memory, which every agent can read. For things the \
116 whole factory needs, not for your own notes."
117 }
118 Self::Status => {
119 "Ask what this chain has left: how many more messages it may send, and how much \
120 budget remains. Worth checking before fanning out to several agents."
121 }
122 Self::Wait => {
123 "Set this work down and have it picked up later, when something you are waiting \
124 for has happened. Nothing is kept running in the meantime."
125 }
126 }
127 }
128
129 #[must_use]
131 pub fn from_name(name: &str) -> Option<Self> {
132 Self::ALL.into_iter().find(|tool| tool.name() == name)
133 }
134
135 #[must_use]
140 pub const fn writes(self) -> bool {
141 matches!(
142 self,
143 Self::Send | Self::MemoryWrite | Self::LogbookAppend | Self::Learn | Self::Wait
144 )
145 }
146}
147
148impl fmt::Display for Tool {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.write_str(self.name())
151 }
152}
153
154#[must_use]
160pub fn unknown_tools_in(text: &str) -> Vec<String> {
161 let mut found = Vec::new();
162
163 for (index, _) in text.match_indices("layover_") {
164 let rest = &text[index..];
165 let end = rest
166 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
167 .unwrap_or(rest.len());
168 let name = &rest[..end];
169
170 if Tool::from_name(name).is_none() && !found.iter().any(|seen| seen == name) {
171 found.push(name.to_owned());
172 }
173 }
174
175 found.sort();
176 found
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn every_tool_has_a_distinct_name() {
185 let mut names: Vec<_> = Tool::ALL.iter().map(|tool| tool.name()).collect();
186 names.sort_unstable();
187 let before = names.len();
188 names.dedup();
189
190 assert_eq!(names.len(), before, "two tools answer to the same name");
191 }
192
193 #[test]
194 fn every_name_round_trips() {
195 for tool in Tool::ALL {
196 assert_eq!(Tool::from_name(tool.name()), Some(tool));
197 }
198 }
199
200 #[test]
201 fn every_tool_is_named_for_layover() {
202 for tool in Tool::ALL {
205 assert!(tool.name().starts_with("layover_"), "{}", tool.name());
206 }
207 }
208
209 #[test]
210 fn every_tool_says_what_it_is_for() {
211 for tool in Tool::ALL {
214 assert!(
215 tool.description().len() > 60,
216 "`{}` needs a description an agent can act on",
217 tool.name()
218 );
219 }
220 }
221
222 #[test]
223 fn there_is_no_spawn_tool() {
224 assert_eq!(Tool::from_name("layover_spawn"), None);
227 }
228
229 #[test]
230 fn a_prompt_naming_a_tool_that_does_not_exist_is_caught() {
231 let prompt = "Investigate, then call layover_publish to ship it.";
232 assert_eq!(unknown_tools_in(prompt), vec!["layover_publish"]);
233 }
234
235 #[test]
236 fn a_prompt_naming_real_tools_is_clean() {
237 let prompt = "Call layover_peers, then layover_send. If stuck, layover_help.";
238 assert!(unknown_tools_in(prompt).is_empty());
239 }
240
241 #[test]
242 fn the_same_wrong_name_twice_is_reported_once() {
243 let prompt = "use layover_ship. then layover_ship again.";
244 assert_eq!(unknown_tools_in(prompt), vec!["layover_ship"]);
245 }
246
247 #[test]
248 fn a_tool_name_followed_by_punctuation_is_still_recognised() {
249 for prompt in [
250 "call layover_send(...)",
251 "call `layover_send`",
252 "call layover_send.",
253 "call layover_send, then wait",
254 ] {
255 assert!(unknown_tools_in(prompt).is_empty(), "{prompt}");
256 }
257 }
258
259 #[test]
260 fn a_read_only_agent_can_be_told_which_tools_would_change_things() {
261 assert!(Tool::Send.writes());
262 assert!(Tool::MemoryWrite.writes());
263 assert!(!Tool::Peers.writes());
264 assert!(!Tool::MemoryRead.writes());
265 assert!(
266 !Tool::Report.writes(),
267 "a report is an account of a run, not an effect on anything else"
268 );
269 }
270}